MDL-28643 Fix debugging message when deleting activity with grade completion
[moodle.git] / lib / grade / grade_grade.php
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Definitions of grade grade class
19  *
20  * @package    core
21  * @subpackage 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 class grade_grade extends grade_object {
32     /**
33      * The DB table.
34      * @var string $table
35      */
36     public $table = 'grade_grades';
38     /**
39      * Array of required table fields, must start with 'id'.
40      * @var array $required_fields
41      */
42     public $required_fields = array('id', 'itemid', 'userid', 'rawgrade', 'rawgrademax', 'rawgrademin',
43                                  'rawscaleid', 'usermodified', 'finalgrade', 'hidden', 'locked',
44                                  'locktime', 'exported', 'overridden', 'excluded', 'timecreated', 'timemodified');
46     /**
47      * Array of optional fields with default values (these should match db defaults)
48      * @var array $optional_fields
49      */
50     public $optional_fields = array('feedback'=>null, 'feedbackformat'=>0, 'information'=>null, 'informationformat'=>0);
52     /**
53      * The id of the grade_item this grade belongs to.
54      * @var int $itemid
55      */
56     public $itemid;
58     /**
59      * The grade_item object referenced by $this->itemid.
60      * @var object $grade_item
61      */
62     public $grade_item;
64     /**
65      * The id of the user this grade belongs to.
66      * @var int $userid
67      */
68     public $userid;
70     /**
71      * The grade value of this raw grade, if such was provided by the module.
72      * @var float $rawgrade
73      */
74     public $rawgrade;
76     /**
77      * The maximum allowable grade when this grade was created.
78      * @var float $rawgrademax
79      */
80     public $rawgrademax = 100;
82     /**
83      * The minimum allowable grade when this grade was created.
84      * @var float $rawgrademin
85      */
86     public $rawgrademin = 0;
88     /**
89      * id of the scale, if this grade is based on a scale.
90      * @var int $rawscaleid
91      */
92     public $rawscaleid;
94     /**
95      * The userid of the person who last modified this grade.
96      * @var int $usermodified
97      */
98     public $usermodified;
100     /**
101      * The final value of this grade.
102      * @var float $finalgrade
103      */
104     public $finalgrade;
106     /**
107      * 0 if visible, 1 always hidden or date not visible until
108      * @var float $hidden
109      */
110     public $hidden = 0;
112     /**
113      * 0 not locked, date when the item was locked
114      * @var float locked
115      */
116     public $locked = 0;
118     /**
119      * 0 no automatic locking, date when to lock the grade automatically
120      * @var float $locktime
121      */
122     public $locktime = 0;
124     /**
125      * Exported flag
126      * @var boolean $exported
127      */
128     public $exported = 0;
130     /**
131      * Overridden flag
132      * @var boolean $overridden
133      */
134     public $overridden = 0;
136     /**
137      * Grade excluded from aggregation functions
138      * @var boolean $excluded
139      */
140     public $excluded = 0;
142     /**
143      * TODO: HACK: create a new field datesubmitted - the date of submission if any
144      * @var boolean $timecreated
145      */
146     public $timecreated = null;
148     /**
149      * TODO: HACK: create a new field dategraded - the date of grading
150      * @var boolean $timemodified
151      */
152     public $timemodified = null;
155     /**
156      * Returns array of grades for given grade_item+users.
157      * @param object $grade_item
158      * @param array $userids
159      * @param bool $include_missing include grades that do not exist yet
160      * @return array userid=>grade_grade array
161      */
162     public static function fetch_users_grades($grade_item, $userids, $include_missing=true) {
163         global $DB;
165         // hmm, there might be a problem with length of sql query
166         // if there are too many users requested - we might run out of memory anyway
167         $limit = 2000;
168         $count = count($userids);
169         if ($count > $limit) {
170             $half = (int)($count/2);
171             $first  = array_slice($userids, 0, $half);
172             $second = array_slice($userids, $half);
173             return grade_grade::fetch_users_grades($grade_item, $first, $include_missing) + grade_grade::fetch_users_grades($grade_item, $second, $include_missing);
174         }
176         list($user_ids_cvs, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'uid0');
177         $params['giid'] = $grade_item->id;
178         $result = array();
179         if ($grade_records = $DB->get_records_select('grade_grades', "itemid=:giid AND userid $user_ids_cvs", $params)) {
180             foreach ($grade_records as $record) {
181                 $result[$record->userid] = new grade_grade($record, false);
182             }
183         }
184         if ($include_missing) {
185             foreach ($userids as $userid) {
186                 if (!array_key_exists($userid, $result)) {
187                     $grade_grade = new grade_grade();
188                     $grade_grade->userid = $userid;
189                     $grade_grade->itemid = $grade_item->id;
190                     $result[$userid] = $grade_grade;
191                 }
192             }
193         }
195         return $result;
196     }
198     /**
199      * Loads the grade_item object referenced by $this->itemid and saves it as $this->grade_item for easy access.
200      * @return object grade_item.
201      */
202     public function load_grade_item() {
203         if (empty($this->itemid)) {
204             debugging('Missing itemid');
205             $this->grade_item = null;
206             return null;
207         }
209         if (empty($this->grade_item)) {
210             $this->grade_item = grade_item::fetch(array('id'=>$this->itemid));
212         } else if ($this->grade_item->id != $this->itemid) {
213             debugging('Itemid mismatch');
214             $this->grade_item = grade_item::fetch(array('id'=>$this->itemid));
215         }
217         return $this->grade_item;
218     }
220     /**
221      * Is grading object editable?
222      * @return boolean
223      */
224     public function is_editable() {
225         if ($this->is_locked()) {
226             return false;
227         }
229         $grade_item = $this->load_grade_item();
231         if ($grade_item->gradetype == GRADE_TYPE_NONE) {
232             return false;
233         }
235         return true;
236     }
238     /**
239      * Check grade lock status. Uses both grade item lock and grade lock.
240      * Internally any date in locked field (including future ones) means locked,
241      * the date is stored for logging purposes only.
242      *
243      * @return boolean true if locked, false if not
244      */
245     public function is_locked() {
246         $this->load_grade_item();
247         if (empty($this->grade_item)) {
248             return !empty($this->locked);
249         } else {
250             return !empty($this->locked) or $this->grade_item->is_locked();
251         }
252     }
254     /**
255      * Checks if grade overridden
256      * @return boolean
257      */
258     public function is_overridden() {
259         return !empty($this->overridden);
260     }
262     /**
263      * Returns timestamp of submission related to this grade,
264      * might be null if not submitted.
265      * @return int
266      */
267     public function get_datesubmitted() {
268         //TODO: HACK - create new fields in 2.0
269         return $this->timecreated;
270     }
272     /**
273      * Returns timestamp when last graded,
274      * might be null if no grade present.
275      * @return int
276      */
277     public function get_dategraded() {
278         //TODO: HACK - create new fields in 2.0
279         if (is_null($this->finalgrade) and is_null($this->feedback)) {
280             return null; // no grade == no date
281         } else if ($this->overridden) {
282             return $this->overridden;
283         } else {
284             return $this->timemodified;
285         }
286     }
288     /**
289      * Set the overridden status of grade
290      * @param boolean $state requested overridden state
291      * @param boolean $refresh refresh grades from external activities if needed
292      * @return boolean true is db state changed
293      */
294     public function set_overridden($state, $refresh = true) {
295         if (empty($this->overridden) and $state) {
296             $this->overridden = time();
297             $this->update();
298             return true;
300         } else if (!empty($this->overridden) and !$state) {
301             $this->overridden = 0;
302             $this->update();
304             if ($refresh) {
305                 //refresh when unlocking
306                 $this->grade_item->refresh_grades($this->userid);
307             }
309             return true;
310         }
311         return false;
312     }
314     /**
315      * Checks if grade excluded from aggregation functions
316      * @return boolean
317      */
318     public function is_excluded() {
319         return !empty($this->excluded);
320     }
322     /**
323      * Set the excluded status of grade
324      * @param boolean $state requested excluded state
325      * @return boolean true is db state changed
326      */
327     public function set_excluded($state) {
328         if (empty($this->excluded) and $state) {
329             $this->excluded = time();
330             $this->update();
331             return true;
333         } else if (!empty($this->excluded) and !$state) {
334             $this->excluded = 0;
335             $this->update();
336             return true;
337         }
338         return false;
339     }
341     /**
342      * Lock/unlock this grade.
343      *
344      * @param int $locked 0, 1 or a timestamp int(10) after which date the item will be locked.
345      * @param boolean $cascade ignored param
346      * @param boolean $refresh refresh grades when unlocking
347      * @return boolean true if successful, false if can not set new lock state for grade
348      */
349     public function set_locked($lockedstate, $cascade=false, $refresh=true) {
350         $this->load_grade_item();
352         if ($lockedstate) {
353             if ($this->grade_item->needsupdate) {
354                 //can not lock grade if final not calculated!
355                 return false;
356             }
358             $this->locked = time();
359             $this->update();
361             return true;
363         } else {
364             if (!empty($this->locked) and $this->locktime < time()) {
365                 //we have to reset locktime or else it would lock up again
366                 $this->locktime = 0;
367             }
369             // remove the locked flag
370             $this->locked = 0;
371             $this->update();
373             if ($refresh and !$this->is_overridden()) {
374                 //refresh when unlocking and not overridden
375                 $this->grade_item->refresh_grades($this->userid);
376             }
378             return true;
379         }
380     }
382     /**
383      * Lock the grade if needed - make sure this is called only when final grades are valid
384      * @param array $items array of all grade item ids
385      * @return void
386      */
387     public function check_locktime_all($items) {
388         global $CFG, $DB;
390         $now = time(); // no rounding needed, this is not supposed to be called every 10 seconds
391         list($usql, $params) = $DB->get_in_or_equal($items);
392         $params[] = $now;
393         $rs = $DB->get_recordset_select('grade_grades', "itemid $usql AND locked = 0 AND locktime > 0 AND locktime < ?", $params);
394         foreach ($rs as $grade) {
395             $grade_grade = new grade_grade($grade, false);
396             $grade_grade->locked = time();
397             $grade_grade->update('locktime');
398         }
399         $rs->close();
400     }
402     /**
403      * Set the locktime for this grade.
404      *
405      * @param int $locktime timestamp for lock to activate
406      * @return void
407      */
408     public function set_locktime($locktime) {
409         $this->locktime = $locktime;
410         $this->update();
411     }
413     /**
414      * Set the locktime for this grade.
415      *
416      * @return int $locktime timestamp for lock to activate
417      */
418     public function get_locktime() {
419         $this->load_grade_item();
421         $item_locktime = $this->grade_item->get_locktime();
423         if (empty($this->locktime) or ($item_locktime and $item_locktime < $this->locktime)) {
424             return $item_locktime;
426         } else {
427             return $this->locktime;
428         }
429     }
431     /**
432      * Check grade hidden status. Uses data from both grade item and grade.
433      * @return boolean true if hidden, false if not
434      */
435     public function is_hidden() {
436         $this->load_grade_item();
437         if (empty($this->grade_item)) {
438             return $this->hidden == 1 or ($this->hidden != 0 and $this->hidden > time());
439         } else {
440             return $this->hidden == 1 or ($this->hidden != 0 and $this->hidden > time()) or $this->grade_item->is_hidden();
441         }
442     }
444     /**
445      * Check grade hidden status. Uses data from both grade item and grade.
446      * @return boolean true if hiddenuntil, false if not
447      */
448     public function is_hiddenuntil() {
449         $this->load_grade_item();
451         if ($this->hidden == 1 or $this->grade_item->hidden == 1) {
452             return false; //always hidden
453         }
455         if ($this->hidden > 1 or $this->grade_item->hidden > 1) {
456             return true;
457         }
459         return false;
460     }
462     /**
463      * Check grade hidden status. Uses data from both grade item and grade.
464      * @return int 0 means visible, 1 hidden always, timestamp hidden until
465      */
466     public function get_hidden() {
467         $this->load_grade_item();
469         $item_hidden = $this->grade_item->get_hidden();
471         if ($item_hidden == 1) {
472             return 1;
474         } else if ($item_hidden == 0) {
475             return $this->hidden;
477         } else {
478             if ($this->hidden == 0) {
479                 return $item_hidden;
480             } else if ($this->hidden == 1) {
481                 return 1;
482             } else if ($this->hidden > $item_hidden) {
483                 return $this->hidden;
484             } else {
485                 return $item_hidden;
486             }
487         }
488     }
490     /**
491      * Set the hidden status of grade, 0 mean visible, 1 always hidden, number means date to hide until.
492      * @param boolean $cascade ignored
493      * @param int $hidden new hidden status
494      */
495     public function set_hidden($hidden, $cascade=false) {
496        $this->hidden = $hidden;
497        $this->update();
498     }
500     /**
501      * Finds and returns a grade_grade instance based on params.
502      * @static
503      *
504      * @param array $params associative arrays varname=>value
505      * @return object grade_grade instance or false if none found.
506      */
507     public static function fetch($params) {
508         return grade_object::fetch_helper('grade_grades', 'grade_grade', $params);
509     }
511     /**
512      * Finds and returns all grade_grade instances based on params.
513      * @static
514      *
515      * @param array $params associative arrays varname=>value
516      * @return array array of grade_grade instances or false if none found.
517      */
518     public static function fetch_all($params) {
519         return grade_object::fetch_all_helper('grade_grades', 'grade_grade', $params);
520     }
522     /**
523      * Given a float value situated between a source minimum and a source maximum, converts it to the
524      * corresponding value situated between a target minimum and a target maximum. Thanks to Darlene
525      * for the formula :-)
526      *
527      * @static
528      * @param float $rawgrade
529      * @param float $source_min
530      * @param float $source_max
531      * @param float $target_min
532      * @param float $target_max
533      * @return float Converted value
534      */
535     public static function standardise_score($rawgrade, $source_min, $source_max, $target_min, $target_max) {
536         if (is_null($rawgrade)) {
537           return null;
538         }
540         if ($source_max == $source_min or $target_min == $target_max) {
541             // prevent division by 0
542             return $target_max;
543         }
545         $factor = ($rawgrade - $source_min) / ($source_max - $source_min);
546         $diff = $target_max - $target_min;
547         $standardised_value = $factor * $diff + $target_min;
548         return $standardised_value;
549     }
551     /**
552      * Return array of grade item ids that are either hidden or indirectly depend
553      * on hidden grades, excluded grades are not returned.
554      * THIS IS A REALLY BIG HACK! to be replaced by conditional aggregation of hidden grades in 2.0
555      *
556      * @static
557      * @param array $grades all course grades of one user, & used for better internal caching
558      * @param array $items $grade_items array of grade items, & used for better internal caching
559      * @return array
560      */
561     public static function get_hiding_affected(&$grade_grades, &$grade_items) {
562         global $CFG;
564         if (count($grade_grades) !== count($grade_items)) {
565             print_error('invalidarraysize', 'debug', '', 'grade_grade::get_hiding_affected()!');
566         }
568         $dependson = array();
569         $todo = array();
570         $unknown = array();  // can not find altered
571         $altered = array();  // altered grades
573         $hiddenfound = false;
574         foreach($grade_grades as $itemid=>$unused) {
575             $grade_grade =& $grade_grades[$itemid];
576             if ($grade_grade->is_excluded()) {
577                 //nothing to do, aggregation is ok
578             } else if ($grade_grade->is_hidden()) {
579                 $hiddenfound = true;
580                 $altered[$grade_grade->itemid] = null;
581             } else if ($grade_grade->is_locked() or $grade_grade->is_overridden()) {
582                 // no need to recalculate locked or overridden grades
583             } else {
584                 $dependson[$grade_grade->itemid] = $grade_items[$grade_grade->itemid]->depends_on();
585                 if (!empty($dependson[$grade_grade->itemid])) {
586                     $todo[] = $grade_grade->itemid;
587                 }
588             }
589         }
590         if (!$hiddenfound) {
591             return array('unknown'=>array(), 'altered'=>array());
592         }
594         $max = count($todo);
595         $hidden_precursors = null;
596         for($i=0; $i<$max; $i++) {
597             $found = false;
598             foreach($todo as $key=>$do) {
599                 $hidden_precursors = array_intersect($dependson[$do], $unknown);
600                 if ($hidden_precursors) {
601                     // this item depends on hidden grade indirectly
602                     $unknown[$do] = $do;
603                     unset($todo[$key]);
604                     $found = true;
605                     continue;
607                 } else if (!array_intersect($dependson[$do], $todo)) {
608                     $hidden_precursors = array_intersect($dependson[$do], array_keys($altered));
609                     if (!$hidden_precursors) {
610                         // hiding does not affect this grade
611                         unset($todo[$key]);
612                         $found = true;
613                         continue;
615                     } else {
616                         // depends on altered grades - we should try to recalculate if possible
617                         if ($grade_items[$do]->is_calculated() or
618                             (!$grade_items[$do]->is_category_item() and !$grade_items[$do]->is_course_item())
619                         ) {
620                             $unknown[$do] = $do;
621                             unset($todo[$key]);
622                             $found = true;
623                             continue;
625                         } else {
626                             $grade_category = $grade_items[$do]->load_item_category();
628                             $values = array();
629                             foreach ($dependson[$do] as $itemid) {
630                                 if (array_key_exists($itemid, $altered)) {
631                                     //nulling an altered precursor
632                                     $values[$itemid] = $altered[$itemid];
633                                 } elseif (empty($values[$itemid])) {
634                                     $values[$itemid] = $grade_grades[$itemid]->finalgrade;
635                                 }
636                             }
638                             foreach ($values as $itemid=>$value) {
639                                 if ($grade_grades[$itemid]->is_excluded()) {
640                                     unset($values[$itemid]);
641                                     continue;
642                                 }
643                                 $values[$itemid] = grade_grade::standardise_score($value, $grade_items[$itemid]->grademin, $grade_items[$itemid]->grademax, 0, 1);
644                             }
646                             if ($grade_category->aggregateonlygraded) {
647                                 foreach ($values as $itemid=>$value) {
648                                     if (is_null($value)) {
649                                         unset($values[$itemid]);
650                                     }
651                                 }
652                             } else {
653                                 foreach ($values as $itemid=>$value) {
654                                     if (is_null($value)) {
655                                         $values[$itemid] = 0;
656                                     }
657                                 }
658                             }
660                             // limit and sort
661                             $grade_category->apply_limit_rules($values, $grade_items);
662                             asort($values, SORT_NUMERIC);
664                             // let's see we have still enough grades to do any statistics
665                             if (count($values) == 0) {
666                                 // not enough attempts yet
667                                 $altered[$do] = null;
668                                 unset($todo[$key]);
669                                 $found = true;
670                                 continue;
671                             }
673                             $agg_grade = $grade_category->aggregate_values($values, $grade_items);
675                             // recalculate the rawgrade back to requested range
676                             $finalgrade = grade_grade::standardise_score($agg_grade, 0, 1, $grade_items[$do]->grademin, $grade_items[$do]->grademax);
678                             $finalgrade = $grade_items[$do]->bounded_grade($finalgrade);
680                             $altered[$do] = $finalgrade;
681                             unset($todo[$key]);
682                             $found = true;
683                             continue;
684                         }
685                     }
686                 }
687             }
688             if (!$found) {
689                 break;
690             }
691         }
693         return array('unknown'=>$unknown, 'altered'=>$altered);
694     }
696     /**
697      * Returns true if the grade's value is superior or equal to the grade item's gradepass value, false otherwise.
698      * @param object $grade_item An optional grade_item of which gradepass value we can use, saves having to load the grade_grade's grade_item
699      * @return boolean
700      */
701     public function is_passed($grade_item = null) {
702         if (empty($grade_item)) {
703             if (!isset($this->grade_item)) {
704                 $this->load_grade_item();
705             }
706         } else {
707             $this->grade_item = $grade_item;
708             $this->itemid = $grade_item->id;
709         }
711         // Return null if finalgrade is null
712         if (is_null($this->finalgrade)) {
713             return null;
714         }
716         // Return null if gradepass == grademin or gradepass is null
717         if (is_null($this->grade_item->gradepass) || $this->grade_item->gradepass == $this->grade_item->grademin) {
718             return null;
719         }
721         return $this->finalgrade >= $this->grade_item->gradepass;
722     }
724     public function insert($source=null) {
725         // TODO: dategraded hack - do not update times, they are used for submission and grading
726         //$this->timecreated = $this->timemodified = time();
727         return parent::insert($source);
728     }
730     /**
731      * In addition to update() as defined in grade_object rounds the float numbers using php function,
732      * the reason is we need to compare the db value with computed number to skip updates if possible.
733      * @param string $source from where was the object inserted (mod/forum, manual, etc.)
734      * @return boolean success
735      */
736     public function update($source=null) {
737         $this->rawgrade    = grade_floatval($this->rawgrade);
738         $this->finalgrade  = grade_floatval($this->finalgrade);
739         $this->rawgrademin = grade_floatval($this->rawgrademin);
740         $this->rawgrademax = grade_floatval($this->rawgrademax);
741         return parent::update($source);
742     }
744     /**
745      * Used to notify the completion system (if necessary) that a user's grade
746      * has changed, and clear up a possible score cache.
747      * @param bool deleted True if grade was actually deleted
748      */
749     function notify_changed($deleted) {
750         global $USER, $SESSION, $CFG,$COURSE, $DB;
752         // Grades may be cached in user session
753         if ($USER->id == $this->userid) {
754             unset($SESSION->gradescorecache[$this->itemid]);
755         }
757         // Ignore during restore
758         // TODO There should be a proper way to determine when we are in restore
759         // so that this hack looking for a $restore global is not needed.
760         global $restore;
761         if (!empty($restore->backup_unique_code)) {
762             return;
763         }
765         require_once($CFG->libdir.'/completionlib.php');
767         // Bail out immediately if completion is not enabled for site (saves loading
768         // grade item below)
769         if (!completion_info::is_enabled_for_site()) {
770             return;
771         }
773         // Load information about grade item
774         $this->load_grade_item();
776         // Only course-modules have completion data
777         if ($this->grade_item->itemtype!='mod') {
778             return;
779         }
781         // Use $COURSE if available otherwise get it via item fields
782         if(!empty($COURSE) && $COURSE->id == $this->grade_item->courseid) {
783             $course = $COURSE;
784         } else {
785             $course = $DB->get_record('course', array('id'=>$this->grade_item->courseid));
786         }
788         // Bail out if completion is not enabled for course
789         $completion = new completion_info($course);
790         if (!$completion->is_enabled()) {
791             return;
792         }
794         // Get course-module
795         $cm = get_coursemodule_from_instance($this->grade_item->itemmodule,
796               $this->grade_item->iteminstance, $this->grade_item->courseid);
797         // If the course-module doesn't exist, display a warning...
798         if (!$cm) {
799             // ...unless the grade is being deleted in which case it's likely
800             // that the course-module was just deleted too, so that's okay.
801             if (!$deleted) {
802                 debugging("Couldn't find course-module for module '" .
803                         $this->grade_item->itemmodule . "', instance '" .
804                         $this->grade_item->iteminstance . "', course '" .
805                         $this->grade_item->courseid . "'");
806             }
807             return;
808         }
810         // Pass information on to completion system
811         $completion->inform_grade_changed($cm, $this->grade_item, $this, $deleted);
812      }