MDL-40697 core_grades: trigger the user_graded event
[moodle.git] / lib / grade / grade_item.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 a grade item
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();
27 require_once('grade_object.php');
29 /**
30  * Class representing a grade item.
31  *
32  * It is responsible for handling its DB representation, modifying and returning its metadata.
33  *
34  * @package   core_grades
35  * @category  grade
36  * @copyright 2006 Nicolas Connault
37  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
38  */
39 class grade_item extends grade_object {
40     /**
41      * DB Table (used by grade_object).
42      * @var string $table
43      */
44     public $table = 'grade_items';
46     /**
47      * Array of required table fields, must start with 'id'.
48      * @var array $required_fields
49      */
50     public $required_fields = array('id', 'courseid', 'categoryid', 'itemname', 'itemtype', 'itemmodule', 'iteminstance',
51                                  'itemnumber', 'iteminfo', 'idnumber', 'calculation', 'gradetype', 'grademax', 'grademin',
52                                  'scaleid', 'outcomeid', 'gradepass', 'multfactor', 'plusfactor', 'aggregationcoef',
53                                  'aggregationcoef2', 'sortorder', 'display', 'decimals', 'hidden', 'locked', 'locktime',
54                                  'needsupdate', 'weightoverride', 'timecreated', 'timemodified');
56     /**
57      * The course this grade_item belongs to.
58      * @var int $courseid
59      */
60     public $courseid;
62     /**
63      * The category this grade_item belongs to (optional).
64      * @var int $categoryid
65      */
66     public $categoryid;
68     /**
69      * The grade_category object referenced $this->iteminstance if itemtype == 'category' or == 'course'.
70      * @var grade_category $item_category
71      */
72     public $item_category;
74     /**
75      * The grade_category object referenced by $this->categoryid.
76      * @var grade_category $parent_category
77      */
78     public $parent_category;
81     /**
82      * The name of this grade_item (pushed by the module).
83      * @var string $itemname
84      */
85     public $itemname;
87     /**
88      * e.g. 'category', 'course' and 'mod', 'blocks', 'import', etc...
89      * @var string $itemtype
90      */
91     public $itemtype;
93     /**
94      * The module pushing this grade (e.g. 'forum', 'quiz', 'assignment' etc).
95      * @var string $itemmodule
96      */
97     public $itemmodule;
99     /**
100      * ID of the item module
101      * @var int $iteminstance
102      */
103     public $iteminstance;
105     /**
106      * Number of the item in a series of multiple grades pushed by an activity.
107      * @var int $itemnumber
108      */
109     public $itemnumber;
111     /**
112      * Info and notes about this item.
113      * @var string $iteminfo
114      */
115     public $iteminfo;
117     /**
118      * Arbitrary idnumber provided by the module responsible.
119      * @var string $idnumber
120      */
121     public $idnumber;
123     /**
124      * Calculation string used for this item.
125      * @var string $calculation
126      */
127     public $calculation;
129     /**
130      * Indicates if we already tried to normalize the grade calculation formula.
131      * This flag helps to minimize db access when broken formulas used in calculation.
132      * @var bool
133      */
134     public $calculation_normalized;
135     /**
136      * Math evaluation object
137      * @var calc_formula A formula object
138      */
139     public $formula;
141     /**
142      * The type of grade (0 = none, 1 = value, 2 = scale, 3 = text)
143      * @var int $gradetype
144      */
145     public $gradetype = GRADE_TYPE_VALUE;
147     /**
148      * Maximum allowable grade.
149      * @var float $grademax
150      */
151     public $grademax = 100;
153     /**
154      * Minimum allowable grade.
155      * @var float $grademin
156      */
157     public $grademin = 0;
159     /**
160      * id of the scale, if this grade is based on a scale.
161      * @var int $scaleid
162      */
163     public $scaleid;
165     /**
166      * The grade_scale object referenced by $this->scaleid.
167      * @var grade_scale $scale
168      */
169     public $scale;
171     /**
172      * The id of the optional grade_outcome associated with this grade_item.
173      * @var int $outcomeid
174      */
175     public $outcomeid;
177     /**
178      * The grade_outcome this grade is associated with, if applicable.
179      * @var grade_outcome $outcome
180      */
181     public $outcome;
183     /**
184      * grade required to pass. (grademin <= gradepass <= grademax)
185      * @var float $gradepass
186      */
187     public $gradepass = 0;
189     /**
190      * Multiply all grades by this number.
191      * @var float $multfactor
192      */
193     public $multfactor = 1.0;
195     /**
196      * Add this to all grades.
197      * @var float $plusfactor
198      */
199     public $plusfactor = 0;
201     /**
202      * Aggregation coeficient used for weighted averages or extra credit
203      * @var float $aggregationcoef
204      */
205     public $aggregationcoef = 0;
207     /**
208      * Aggregation coeficient used for weighted averages only
209      * @var float $aggregationcoef2
210      */
211     public $aggregationcoef2 = 0;
213     /**
214      * Sorting order of the columns.
215      * @var int $sortorder
216      */
217     public $sortorder = 0;
219     /**
220      * Display type of the grades (Real, Percentage, Letter, or default).
221      * @var int $display
222      */
223     public $display = GRADE_DISPLAY_TYPE_DEFAULT;
225     /**
226      * The number of digits after the decimal point symbol. Applies only to REAL and PERCENTAGE grade display types.
227      * @var int $decimals
228      */
229     public $decimals = null;
231     /**
232      * Grade item lock flag. Empty if not locked, locked if any value present, usually date when item was locked. Locking prevents updating.
233      * @var int $locked
234      */
235     public $locked = 0;
237     /**
238      * Date after which the grade will be locked. Empty means no automatic locking.
239      * @var int $locktime
240      */
241     public $locktime = 0;
243     /**
244      * If set, the whole column will be recalculated, then this flag will be switched off.
245      * @var bool $needsupdate
246      */
247     public $needsupdate = 1;
249     /**
250      * If set, the grade item's weight has been overridden by a user and should not be automatically adjusted.
251      */
252     public $weightoverride = 0;
254     /**
255      * Cached dependson array
256      * @var array An array of cached grade item dependencies.
257      */
258     public $dependson_cache = null;
260     /**
261      * In addition to update() as defined in grade_object, handle the grade_outcome and grade_scale objects.
262      * Force regrading if necessary, rounds the float numbers using php function,
263      * the reason is we need to compare the db value with computed number to skip regrading if possible.
264      *
265      * @param string $source from where was the object inserted (mod/forum, manual, etc.)
266      * @return bool success
267      */
268     public function update($source=null) {
269         // reset caches
270         $this->dependson_cache = null;
272         // Retrieve scale and infer grademax/min from it if needed
273         $this->load_scale();
275         // make sure there is not 0 in outcomeid
276         if (empty($this->outcomeid)) {
277             $this->outcomeid = null;
278         }
280         if ($this->qualifies_for_regrading()) {
281             $this->force_regrading();
282         }
284         $this->timemodified = time();
286         $this->grademin        = grade_floatval($this->grademin);
287         $this->grademax        = grade_floatval($this->grademax);
288         $this->multfactor      = grade_floatval($this->multfactor);
289         $this->plusfactor      = grade_floatval($this->plusfactor);
290         $this->aggregationcoef = grade_floatval($this->aggregationcoef);
291         $this->aggregationcoef2 = grade_floatval($this->aggregationcoef2);
293         return parent::update($source);
294     }
296     /**
297      * Compares the values held by this object with those of the matching record in DB, and returns
298      * whether or not these differences are sufficient to justify an update of all parent objects.
299      * This assumes that this object has an id number and a matching record in DB. If not, it will return false.
300      *
301      * @return bool
302      */
303     public function qualifies_for_regrading() {
304         if (empty($this->id)) {
305             return false;
306         }
308         $db_item = new grade_item(array('id' => $this->id));
310         $calculationdiff = $db_item->calculation != $this->calculation;
311         $categorydiff    = $db_item->categoryid  != $this->categoryid;
312         $gradetypediff   = $db_item->gradetype   != $this->gradetype;
313         $scaleiddiff     = $db_item->scaleid     != $this->scaleid;
314         $outcomeiddiff   = $db_item->outcomeid   != $this->outcomeid;
315         $locktimediff    = $db_item->locktime    != $this->locktime;
316         $grademindiff    = grade_floats_different($db_item->grademin,        $this->grademin);
317         $grademaxdiff    = grade_floats_different($db_item->grademax,        $this->grademax);
318         $multfactordiff  = grade_floats_different($db_item->multfactor,      $this->multfactor);
319         $plusfactordiff  = grade_floats_different($db_item->plusfactor,      $this->plusfactor);
320         $acoefdiff       = grade_floats_different($db_item->aggregationcoef, $this->aggregationcoef);
321         $acoefdiff2      = grade_floats_different($db_item->aggregationcoef2, $this->aggregationcoef2);
322         $weightoverride  = grade_floats_different($db_item->weightoverride, $this->weightoverride);
324         $needsupdatediff = !$db_item->needsupdate &&  $this->needsupdate;    // force regrading only if setting the flag first time
325         $lockeddiff      = !empty($db_item->locked) && empty($this->locked); // force regrading only when unlocking
327         return ($calculationdiff || $categorydiff || $gradetypediff || $grademaxdiff || $grademindiff || $scaleiddiff
328              || $outcomeiddiff || $multfactordiff || $plusfactordiff || $needsupdatediff
329              || $lockeddiff || $acoefdiff || $acoefdiff2 || $weightoverride || $locktimediff);
330     }
332     /**
333      * Finds and returns a grade_item instance based on params.
334      *
335      * @static
336      * @param array $params associative arrays varname=>value
337      * @return grade_item|bool Returns a grade_item instance or false if none found
338      */
339     public static function fetch($params) {
340         return grade_object::fetch_helper('grade_items', 'grade_item', $params);
341     }
343     /**
344      * Finds and returns all grade_item instances based on params.
345      *
346      * @static
347      * @param array $params associative arrays varname=>value
348      * @return array array of grade_item instances or false if none found.
349      */
350     public static function fetch_all($params) {
351         return grade_object::fetch_all_helper('grade_items', 'grade_item', $params);
352     }
354     /**
355      * Delete all grades and force_regrading of parent category.
356      *
357      * @param string $source from where was the object deleted (mod/forum, manual, etc.)
358      * @return bool success
359      */
360     public function delete($source=null) {
361         $this->delete_all_grades($source);
362         return parent::delete($source);
363     }
365     /**
366      * Delete all grades
367      *
368      * @param string $source from where was the object deleted (mod/forum, manual, etc.)
369      * @return bool
370      */
371     public function delete_all_grades($source=null) {
372         if (!$this->is_course_item()) {
373             $this->force_regrading();
374         }
376         if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
377             foreach ($grades as $grade) {
378                 $grade->delete($source);
379             }
380         }
382         return true;
383     }
385     /**
386      * In addition to perform parent::insert(), calls force_regrading() method too.
387      *
388      * @param string $source From where was the object inserted (mod/forum, manual, etc.)
389      * @return int PK ID if successful, false otherwise
390      */
391     public function insert($source=null) {
392         global $CFG, $DB;
394         if (empty($this->courseid)) {
395             print_error('cannotinsertgrade');
396         }
398         // load scale if needed
399         $this->load_scale();
401         // add parent category if needed
402         if (empty($this->categoryid) and !$this->is_course_item() and !$this->is_category_item()) {
403             $course_category = grade_category::fetch_course_category($this->courseid);
404             $this->categoryid = $course_category->id;
406         }
408         // always place the new items at the end, move them after insert if needed
409         $last_sortorder = $DB->get_field_select('grade_items', 'MAX(sortorder)', "courseid = ?", array($this->courseid));
410         if (!empty($last_sortorder)) {
411             $this->sortorder = $last_sortorder + 1;
412         } else {
413             $this->sortorder = 1;
414         }
416         // add proper item numbers to manual items
417         if ($this->itemtype == 'manual') {
418             if (empty($this->itemnumber)) {
419                 $this->itemnumber = 0;
420             }
421         }
423         // make sure there is not 0 in outcomeid
424         if (empty($this->outcomeid)) {
425             $this->outcomeid = null;
426         }
428         $this->timecreated = $this->timemodified = time();
430         if (parent::insert($source)) {
431             // force regrading of items if needed
432             $this->force_regrading();
433             return $this->id;
435         } else {
436             debugging("Could not insert this grade_item in the database!");
437             return false;
438         }
439     }
441     /**
442      * Set idnumber of grade item, updates also course_modules table
443      *
444      * @param string $idnumber (without magic quotes)
445      * @return bool success
446      */
447     public function add_idnumber($idnumber) {
448         global $DB;
449         if (!empty($this->idnumber)) {
450             return false;
451         }
453         if ($this->itemtype == 'mod' and !$this->is_outcome_item()) {
454             if ($this->itemnumber == 0) {
455                 // for activity modules, itemnumber 0 is synced with the course_modules
456                 if (!$cm = get_coursemodule_from_instance($this->itemmodule, $this->iteminstance, $this->courseid)) {
457                     return false;
458                 }
459                 if (!empty($cm->idnumber)) {
460                     return false;
461                 }
462                 $DB->set_field('course_modules', 'idnumber', $idnumber, array('id' => $cm->id));
463                 $this->idnumber = $idnumber;
464                 return $this->update();
465             } else {
466                 $this->idnumber = $idnumber;
467                 return $this->update();
468             }
470         } else {
471             $this->idnumber = $idnumber;
472             return $this->update();
473         }
474     }
476     /**
477      * Returns the locked state of this grade_item (if the grade_item is locked OR no specific
478      * $userid is given) or the locked state of a specific grade within this item if a specific
479      * $userid is given and the grade_item is unlocked.
480      *
481      * @param int $userid The user's ID
482      * @return bool Locked state
483      */
484     public function is_locked($userid=NULL) {
485         if (!empty($this->locked)) {
486             return true;
487         }
489         if (!empty($userid)) {
490             if ($grade = grade_grade::fetch(array('itemid'=>$this->id, 'userid'=>$userid))) {
491                 $grade->grade_item =& $this; // prevent db fetching of cached grade_item
492                 return $grade->is_locked();
493             }
494         }
496         return false;
497     }
499     /**
500      * Locks or unlocks this grade_item and (optionally) all its associated final grades.
501      *
502      * @param int $lockedstate 0, 1 or a timestamp int(10) after which date the item will be locked.
503      * @param bool $cascade Lock/unlock child objects too
504      * @param bool $refresh Refresh grades when unlocking
505      * @return bool True if grade_item all grades updated, false if at least one update fails
506      */
507     public function set_locked($lockedstate, $cascade=false, $refresh=true) {
508         if ($lockedstate) {
509         /// setting lock
510             if ($this->needsupdate) {
511                 return false; // can not lock grade without first having final grade
512             }
514             $this->locked = time();
515             $this->update();
517             if ($cascade) {
518                 $grades = $this->get_final();
519                 foreach($grades as $g) {
520                     $grade = new grade_grade($g, false);
521                     $grade->grade_item =& $this;
522                     $grade->set_locked(1, null, false);
523                 }
524             }
526             return true;
528         } else {
529         /// removing lock
530             if (!empty($this->locked) and $this->locktime < time()) {
531                 //we have to reset locktime or else it would lock up again
532                 $this->locktime = 0;
533             }
535             $this->locked = 0;
536             $this->update();
538             if ($cascade) {
539                 if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
540                     foreach($grades as $grade) {
541                         $grade->grade_item =& $this;
542                         $grade->set_locked(0, null, false);
543                     }
544                 }
545             }
547             if ($refresh) {
548                 //refresh when unlocking
549                 $this->refresh_grades();
550             }
552             return true;
553         }
554     }
556     /**
557      * Lock the grade if needed. Make sure this is called only when final grades are valid
558      */
559     public function check_locktime() {
560         if (!empty($this->locked)) {
561             return; // already locked
562         }
564         if ($this->locktime and $this->locktime < time()) {
565             $this->locked = time();
566             $this->update('locktime');
567         }
568     }
570     /**
571      * Set the locktime for this grade item.
572      *
573      * @param int $locktime timestamp for lock to activate
574      * @return void
575      */
576     public function set_locktime($locktime) {
577         $this->locktime = $locktime;
578         $this->update();
579     }
581     /**
582      * Set the locktime for this grade item.
583      *
584      * @return int $locktime timestamp for lock to activate
585      */
586     public function get_locktime() {
587         return $this->locktime;
588     }
590     /**
591      * Set the hidden status of grade_item and all grades.
592      *
593      * 0 mean always visible, 1 means always hidden and a number > 1 is a timestamp to hide until
594      *
595      * @param int $hidden new hidden status
596      * @param bool $cascade apply to child objects too
597      */
598     public function set_hidden($hidden, $cascade=false) {
599         parent::set_hidden($hidden, $cascade);
601         if ($cascade) {
602             if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
603                 foreach($grades as $grade) {
604                     $grade->grade_item =& $this;
605                     $grade->set_hidden($hidden, $cascade);
606                 }
607             }
608         }
610         //if marking item visible make sure category is visible MDL-21367
611         if( !$hidden ) {
612             $category_array = grade_category::fetch_all(array('id'=>$this->categoryid));
613             if ($category_array && array_key_exists($this->categoryid, $category_array)) {
614                 $category = $category_array[$this->categoryid];
615                 //call set_hidden on the category regardless of whether it is hidden as its parent might be hidden
616                 //if($category->is_hidden()) {
617                     $category->set_hidden($hidden, false);
618                 //}
619             }
620         }
621     }
623     /**
624      * Returns the number of grades that are hidden
625      *
626      * @param string $groupsql SQL to limit the query by group
627      * @param array $params SQL params for $groupsql
628      * @param string $groupwheresql Where conditions for $groupsql
629      * @return int The number of hidden grades
630      */
631     public function has_hidden_grades($groupsql="", array $params=null, $groupwheresql="") {
632         global $DB;
633         $params = (array)$params;
634         $params['itemid'] = $this->id;
636         return $DB->get_field_sql("SELECT COUNT(*) FROM {grade_grades} g LEFT JOIN "
637                             ."{user} u ON g.userid = u.id $groupsql WHERE itemid = :itemid AND hidden = 1 $groupwheresql", $params);
638     }
640     /**
641      * Mark regrading as finished successfully.
642      */
643     public function regrading_finished() {
644         global $DB;
645         $this->needsupdate = 0;
646         //do not use $this->update() because we do not want this logged in grade_item_history
647         $DB->set_field('grade_items', 'needsupdate', 0, array('id' => $this->id));
648     }
650     /**
651      * Performs the necessary calculations on the grades_final referenced by this grade_item.
652      * Also resets the needsupdate flag once successfully performed.
653      *
654      * This function must be used ONLY from lib/gradeslib.php/grade_regrade_final_grades(),
655      * because the regrading must be done in correct order!!
656      *
657      * @param int $userid Supply a user ID to limit the regrading to a single user
658      * @return bool true if ok, error string otherwise
659      */
660     public function regrade_final_grades($userid=null) {
661         global $CFG, $DB;
663         // locked grade items already have correct final grades
664         if ($this->is_locked()) {
665             return true;
666         }
668         // calculation produces final value using formula from other final values
669         if ($this->is_calculated()) {
670             if ($this->compute($userid)) {
671                 return true;
672             } else {
673                 return "Could not calculate grades for grade item"; // TODO: improve and localize
674             }
676         // noncalculated outcomes already have final values - raw grades not used
677         } else if ($this->is_outcome_item()) {
678             return true;
680         // aggregate the category grade
681         } else if ($this->is_category_item() or $this->is_course_item()) {
682             // aggregate category grade item
683             $category = $this->get_item_category();
684             $category->grade_item =& $this;
685             if ($category->generate_grades($userid)) {
686                 return true;
687             } else {
688                 return "Could not aggregate final grades for category:".$this->id; // TODO: improve and localize
689             }
691         } else if ($this->is_manual_item()) {
692             // manual items track only final grades, no raw grades
693             return true;
695         } else if (!$this->is_raw_used()) {
696             // hmm - raw grades are not used- nothing to regrade
697             return true;
698         }
700         // normal grade item - just new final grades
701         $result = true;
702         $grade_inst = new grade_grade();
703         $fields = implode(',', $grade_inst->required_fields);
704         if ($userid) {
705             $params = array($this->id, $userid);
706             $rs = $DB->get_recordset_select('grade_grades', "itemid=? AND userid=?", $params, '', $fields);
707         } else {
708             $rs = $DB->get_recordset('grade_grades', array('itemid' => $this->id), '', $fields);
709         }
710         if ($rs) {
711             foreach ($rs as $grade_record) {
712                 $grade = new grade_grade($grade_record, false);
714                 if (!empty($grade_record->locked) or !empty($grade_record->overridden)) {
715                     // this grade is locked - final grade must be ok
716                     continue;
717                 }
719                 $grade->finalgrade = $this->adjust_raw_grade($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
721                 if (grade_floats_different($grade_record->finalgrade, $grade->finalgrade)) {
722                     $success = $grade->update('system');
724                     // If successful trigger a user_graded event.
725                     if ($success) {
726                         $grade->load_grade_item();
727                         \core\event\user_graded::create_from_grade($grade)->trigger();
728                     } else {
729                         $result = "Internal error updating final grade";
730                     }
731                 }
732             }
733             $rs->close();
734         }
736         return $result;
737     }
739     /**
740      * Given a float grade value or integer grade scale, applies a number of adjustment based on
741      * grade_item variables and returns the result.
742      *
743      * @param float $rawgrade The raw grade value
744      * @param float $rawmin original rawmin
745      * @param float $rawmax original rawmax
746      * @return mixed
747      */
748     public function adjust_raw_grade($rawgrade, $rawmin, $rawmax) {
749         if (is_null($rawgrade)) {
750             return null;
751         }
753         if ($this->gradetype == GRADE_TYPE_VALUE) { // Dealing with numerical grade
755             if ($this->grademax < $this->grademin) {
756                 return null;
757             }
759             if ($this->grademax == $this->grademin) {
760                 return $this->grademax; // no range
761             }
763             // Standardise score to the new grade range
764             // NOTE: this is not compatible with current assignment grading
765             $isassignmentmodule = ($this->itemmodule == 'assignment') || ($this->itemmodule == 'assign');
766             if (!$isassignmentmodule && ($rawmin != $this->grademin or $rawmax != $this->grademax)) {
767                 $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax);
768             }
770             // Apply other grade_item factors
771             $rawgrade *= $this->multfactor;
772             $rawgrade += $this->plusfactor;
774             return $this->bounded_grade($rawgrade);
776         } else if ($this->gradetype == GRADE_TYPE_SCALE) { // Dealing with a scale value
777             if (empty($this->scale)) {
778                 $this->load_scale();
779             }
781             if ($this->grademax < 0) {
782                 return null; // scale not present - no grade
783             }
785             if ($this->grademax == 0) {
786                 return $this->grademax; // only one option
787             }
789             // Convert scale if needed
790             // NOTE: this is not compatible with current assignment grading
791             if ($this->itemmodule != 'assignment' and ($rawmin != $this->grademin or $rawmax != $this->grademax)) {
792                 $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax);
793             }
795             return $this->bounded_grade($rawgrade);
798         } else if ($this->gradetype == GRADE_TYPE_TEXT or $this->gradetype == GRADE_TYPE_NONE) { // no value
799             // somebody changed the grading type when grades already existed
800             return null;
802         } else {
803             debugging("Unknown grade type");
804             return null;
805         }
806     }
808     /**
809      * Sets this grade_item's needsupdate to true. Also marks the course item as needing update.
810      *
811      * @return void
812      */
813     public function force_regrading() {
814         global $DB;
815         $this->needsupdate = 1;
816         //mark this item and course item only - categories and calculated items are always regraded
817         $wheresql = "(itemtype='course' OR id=?) AND courseid=?";
818         $params   = array($this->id, $this->courseid);
819         $DB->set_field_select('grade_items', 'needsupdate', 1, $wheresql, $params);
820     }
822     /**
823      * Instantiates a grade_scale object from the DB if this item's scaleid variable is set
824      *
825      * @return grade_scale Returns a grade_scale object or null if no scale used
826      */
827     public function load_scale() {
828         if ($this->gradetype != GRADE_TYPE_SCALE) {
829             $this->scaleid = null;
830         }
832         if (!empty($this->scaleid)) {
833             //do not load scale if already present
834             if (empty($this->scale->id) or $this->scale->id != $this->scaleid) {
835                 $this->scale = grade_scale::fetch(array('id'=>$this->scaleid));
836                 if (!$this->scale) {
837                     debugging('Incorrect scale id: '.$this->scaleid);
838                     $this->scale = null;
839                     return null;
840                 }
841                 $this->scale->load_items();
842             }
844             // Until scales are uniformly set to min=0 max=count(scaleitems)-1 throughout Moodle, we
845             // stay with the current min=1 max=count(scaleitems)
846             $this->grademax = count($this->scale->scale_items);
847             $this->grademin = 1;
849         } else {
850             $this->scale = null;
851         }
853         return $this->scale;
854     }
856     /**
857      * Instantiates a grade_outcome object from the DB if this item's outcomeid variable is set
858      *
859      * @return grade_outcome This grade item's associated grade_outcome or null
860      */
861     public function load_outcome() {
862         if (!empty($this->outcomeid)) {
863             $this->outcome = grade_outcome::fetch(array('id'=>$this->outcomeid));
864         }
865         return $this->outcome;
866     }
868     /**
869      * Returns the grade_category object this grade_item belongs to (referenced by categoryid)
870      * or category attached to category item.
871      *
872      * @return grade_category|bool Returns a grade_category object if applicable or false if this is a course item
873      */
874     public function get_parent_category() {
875         if ($this->is_category_item() or $this->is_course_item()) {
876             return $this->get_item_category();
878         } else {
879             return grade_category::fetch(array('id'=>$this->categoryid));
880         }
881     }
883     /**
884      * Calls upon the get_parent_category method to retrieve the grade_category object
885      * from the DB and assigns it to $this->parent_category. It also returns the object.
886      *
887      * @return grade_category This grade item's parent grade_category.
888      */
889     public function load_parent_category() {
890         if (empty($this->parent_category->id)) {
891             $this->parent_category = $this->get_parent_category();
892         }
893         return $this->parent_category;
894     }
896     /**
897      * Returns the grade_category for a grade category grade item
898      *
899      * @return grade_category|bool Returns a grade_category instance if applicable or false otherwise
900      */
901     public function get_item_category() {
902         if (!$this->is_course_item() and !$this->is_category_item()) {
903             return false;
904         }
905         return grade_category::fetch(array('id'=>$this->iteminstance));
906     }
908     /**
909      * Calls upon the get_item_category method to retrieve the grade_category object
910      * from the DB and assigns it to $this->item_category. It also returns the object.
911      *
912      * @return grade_category
913      */
914     public function load_item_category() {
915         if (empty($this->item_category->id)) {
916             $this->item_category = $this->get_item_category();
917         }
918         return $this->item_category;
919     }
921     /**
922      * Is the grade item associated with category?
923      *
924      * @return bool
925      */
926     public function is_category_item() {
927         return ($this->itemtype == 'category');
928     }
930     /**
931      * Is the grade item associated with course?
932      *
933      * @return bool
934      */
935     public function is_course_item() {
936         return ($this->itemtype == 'course');
937     }
939     /**
940      * Is this a manually graded item?
941      *
942      * @return bool
943      */
944     public function is_manual_item() {
945         return ($this->itemtype == 'manual');
946     }
948     /**
949      * Is this an outcome item?
950      *
951      * @return bool
952      */
953     public function is_outcome_item() {
954         return !empty($this->outcomeid);
955     }
957     /**
958      * Is the grade item external - associated with module, plugin or something else?
959      *
960      * @return bool
961      */
962     public function is_external_item() {
963         return ($this->itemtype == 'mod');
964     }
966     /**
967      * Is the grade item overridable
968      *
969      * @return bool
970      */
971     public function is_overridable_item() {
972         if ($this->is_course_item() or $this->is_category_item()) {
973             $overridable = (bool) get_config('moodle', 'grade_overridecat');
974         } else {
975             $overridable = false;
976         }
978         return !$this->is_outcome_item() and ($this->is_external_item() or $this->is_calculated() or $overridable);
979     }
981     /**
982      * Is the grade item feedback overridable
983      *
984      * @return bool
985      */
986     public function is_overridable_item_feedback() {
987         return !$this->is_outcome_item() and $this->is_external_item();
988     }
990     /**
991      * Returns true if grade items uses raw grades
992      *
993      * @return bool
994      */
995     public function is_raw_used() {
996         return ($this->is_external_item() and !$this->is_calculated() and !$this->is_outcome_item());
997     }
999     /**
1000      * Returns the grade item associated with the course
1001      *
1002      * @param int $courseid
1003      * @return grade_item Course level grade item object
1004      */
1005     public static function fetch_course_item($courseid) {
1006         if ($course_item = grade_item::fetch(array('courseid'=>$courseid, 'itemtype'=>'course'))) {
1007             return $course_item;
1008         }
1010         // first get category - it creates the associated grade item
1011         $course_category = grade_category::fetch_course_category($courseid);
1012         return $course_category->get_grade_item();
1013     }
1015     /**
1016      * Is grading object editable?
1017      *
1018      * @return bool
1019      */
1020     public function is_editable() {
1021         return true;
1022     }
1024     /**
1025      * Checks if grade calculated. Returns this object's calculation.
1026      *
1027      * @return bool true if grade item calculated.
1028      */
1029     public function is_calculated() {
1030         if (empty($this->calculation)) {
1031             return false;
1032         }
1034         /*
1035          * The main reason why we use the ##gixxx## instead of [[idnumber]] is speed of depends_on(),
1036          * we would have to fetch all course grade items to find out the ids.
1037          * Also if user changes the idnumber the formula does not need to be updated.
1038          */
1040         // first detect if we need to change calculation formula from [[idnumber]] to ##giXXX## (after backup, etc.)
1041         if (!$this->calculation_normalized and strpos($this->calculation, '[[') !== false) {
1042             $this->set_calculation($this->calculation);
1043         }
1045         return !empty($this->calculation);
1046     }
1048     /**
1049      * Returns calculation string if grade calculated.
1050      *
1051      * @return string Returns the grade item's calculation if calculation is used, null if not
1052      */
1053     public function get_calculation() {
1054         if ($this->is_calculated()) {
1055             return grade_item::denormalize_formula($this->calculation, $this->courseid);
1057         } else {
1058             return NULL;
1059         }
1060     }
1062     /**
1063      * Sets this item's calculation (creates it) if not yet set, or
1064      * updates it if already set (in the DB). If no calculation is given,
1065      * the calculation is removed.
1066      *
1067      * @param string $formula string representation of formula used for calculation
1068      * @return bool success
1069      */
1070     public function set_calculation($formula) {
1071         $this->calculation = grade_item::normalize_formula($formula, $this->courseid);
1072         $this->calculation_normalized = true;
1073         return $this->update();
1074     }
1076     /**
1077      * Denormalizes the calculation formula to [idnumber] form
1078      *
1079      * @param string $formula A string representation of the formula
1080      * @param int $courseid The course ID
1081      * @return string The denormalized formula as a string
1082      */
1083     public static function denormalize_formula($formula, $courseid) {
1084         if (empty($formula)) {
1085             return '';
1086         }
1088         // denormalize formula - convert ##giXX## to [[idnumber]]
1089         if (preg_match_all('/##gi(\d+)##/', $formula, $matches)) {
1090             foreach ($matches[1] as $id) {
1091                 if ($grade_item = grade_item::fetch(array('id'=>$id, 'courseid'=>$courseid))) {
1092                     if (!empty($grade_item->idnumber)) {
1093                         $formula = str_replace('##gi'.$grade_item->id.'##', '[['.$grade_item->idnumber.']]', $formula);
1094                     }
1095                 }
1096             }
1097         }
1099         return $formula;
1101     }
1103     /**
1104      * Normalizes the calculation formula to [#giXX#] form
1105      *
1106      * @param string $formula The formula
1107      * @param int $courseid The course ID
1108      * @return string The normalized formula as a string
1109      */
1110     public static function normalize_formula($formula, $courseid) {
1111         $formula = trim($formula);
1113         if (empty($formula)) {
1114             return NULL;
1116         }
1118         // normalize formula - we want grade item ids ##giXXX## instead of [[idnumber]]
1119         if ($grade_items = grade_item::fetch_all(array('courseid'=>$courseid))) {
1120             foreach ($grade_items as $grade_item) {
1121                 $formula = str_replace('[['.$grade_item->idnumber.']]', '##gi'.$grade_item->id.'##', $formula);
1122             }
1123         }
1125         return $formula;
1126     }
1128     /**
1129      * Returns the final values for this grade item (as imported by module or other source).
1130      *
1131      * @param int $userid Optional: to retrieve a single user's final grade
1132      * @return array|grade_grade An array of all grade_grade instances for this grade_item, or a single grade_grade instance.
1133      */
1134     public function get_final($userid=NULL) {
1135         global $DB;
1136         if ($userid) {
1137             if ($user = $DB->get_record('grade_grades', array('itemid' => $this->id, 'userid' => $userid))) {
1138                 return $user;
1139             }
1141         } else {
1142             if ($grades = $DB->get_records('grade_grades', array('itemid' => $this->id))) {
1143                 //TODO: speed up with better SQL (MDL-31380)
1144                 $result = array();
1145                 foreach ($grades as $grade) {
1146                     $result[$grade->userid] = $grade;
1147                 }
1148                 return $result;
1149             } else {
1150                 return array();
1151             }
1152         }
1153     }
1155     /**
1156      * Get (or create if not exist yet) grade for this user
1157      *
1158      * @param int $userid The user ID
1159      * @param bool $create If true and the user has no grade for this grade item a new grade_grade instance will be inserted
1160      * @return grade_grade The grade_grade instance for the user for this grade item
1161      */
1162     public function get_grade($userid, $create=true) {
1163         if (empty($this->id)) {
1164             debugging('Can not use before insert');
1165             return false;
1166         }
1168         $grade = new grade_grade(array('userid'=>$userid, 'itemid'=>$this->id));
1169         if (empty($grade->id) and $create) {
1170             $grade->insert();
1171         }
1173         return $grade;
1174     }
1176     /**
1177      * Returns the sortorder of this grade_item. This method is also available in
1178      * grade_category, for cases where the object type is not know.
1179      *
1180      * @return int Sort order
1181      */
1182     public function get_sortorder() {
1183         return $this->sortorder;
1184     }
1186     /**
1187      * Returns the idnumber of this grade_item. This method is also available in
1188      * grade_category, for cases where the object type is not know.
1189      *
1190      * @return string The grade item idnumber
1191      */
1192     public function get_idnumber() {
1193         return $this->idnumber;
1194     }
1196     /**
1197      * Returns this grade_item. This method is also available in
1198      * grade_category, for cases where the object type is not know.
1199      *
1200      * @return grade_item
1201      */
1202     public function get_grade_item() {
1203         return $this;
1204     }
1206     /**
1207      * Sets the sortorder of this grade_item. This method is also available in
1208      * grade_category, for cases where the object type is not know.
1209      *
1210      * @param int $sortorder
1211      */
1212     public function set_sortorder($sortorder) {
1213         if ($this->sortorder == $sortorder) {
1214             return;
1215         }
1216         $this->sortorder = $sortorder;
1217         $this->update();
1218     }
1220     /**
1221      * Update this grade item's sortorder so that it will appear after $sortorder
1222      *
1223      * @param int $sortorder The sort order to place this grade item after
1224      */
1225     public function move_after_sortorder($sortorder) {
1226         global $CFG, $DB;
1228         //make some room first
1229         $params = array($sortorder, $this->courseid);
1230         $sql = "UPDATE {grade_items}
1231                    SET sortorder = sortorder + 1
1232                  WHERE sortorder > ? AND courseid = ?";
1233         $DB->execute($sql, $params);
1235         $this->set_sortorder($sortorder + 1);
1236     }
1238     /**
1239      * Detect duplicate grade item's sortorder and re-sort them.
1240      * Note: Duplicate sortorder will be introduced while duplicating activities or
1241      * merging two courses.
1242      *
1243      * @param int $courseid id of the course for which grade_items sortorder need to be fixed.
1244      */
1245     public static function fix_duplicate_sortorder($courseid) {
1246         global $DB;
1248         $transaction = $DB->start_delegated_transaction();
1250         $sql = "SELECT DISTINCT g1.id, g1.courseid, g1.sortorder
1251                     FROM {grade_items} g1
1252                     JOIN {grade_items} g2 ON g1.courseid = g2.courseid
1253                 WHERE g1.sortorder = g2.sortorder AND g1.id != g2.id AND g1.courseid = :courseid
1254                 ORDER BY g1.sortorder DESC, g1.id DESC";
1256         // Get all duplicates in course highest sort order, and higest id first so that we can make space at the
1257         // bottom higher end of the sort orders and work down by id.
1258         $rs = $DB->get_recordset_sql($sql, array('courseid' => $courseid));
1260         foreach($rs as $duplicate) {
1261             $DB->execute("UPDATE {grade_items}
1262                             SET sortorder = sortorder + 1
1263                           WHERE courseid = :courseid AND
1264                           (sortorder > :sortorder OR (sortorder = :sortorder2 AND id > :id))",
1265                 array('courseid' => $duplicate->courseid,
1266                     'sortorder' => $duplicate->sortorder,
1267                     'sortorder2' => $duplicate->sortorder,
1268                     'id' => $duplicate->id));
1269         }
1270         $rs->close();
1271         $transaction->allow_commit();
1272     }
1274     /**
1275      * Returns the most descriptive field for this object.
1276      *
1277      * Determines what type of grade item it is then returns the appropriate string
1278      *
1279      * @param bool $fulltotal If the item is a category total, returns $categoryname."total" instead of "Category total" or "Course total"
1280      * @return string name
1281      */
1282     public function get_name($fulltotal=false) {
1283         if (!empty($this->itemname)) {
1284             // MDL-10557
1285             return format_string($this->itemname);
1287         } else if ($this->is_course_item()) {
1288             return get_string('coursetotal', 'grades');
1290         } else if ($this->is_category_item()) {
1291             if ($fulltotal) {
1292                 $category = $this->load_parent_category();
1293                 $a = new stdClass();
1294                 $a->category = $category->get_name();
1295                 return get_string('categorytotalfull', 'grades', $a);
1296             } else {
1297             return get_string('categorytotal', 'grades');
1298             }
1300         } else {
1301             return get_string('grade');
1302         }
1303     }
1305     /**
1306      * A grade item can return a more detailed description which will be added to the header of the column/row in some reports.
1307      *
1308      * @return string description
1309      */
1310     public function get_description() {
1311         if ($this->is_course_item() || $this->is_category_item()) {
1312             $categoryitem = $this->load_item_category();
1313             return $categoryitem->get_description();
1314         }
1315         return '';
1316     }
1318     /**
1319      * Sets this item's categoryid. A generic method shared by objects that have a parent id of some kind.
1320      *
1321      * @param int $parentid The ID of the new parent
1322      * @return bool True if success
1323      */
1324     public function set_parent($parentid) {
1325         if ($this->is_course_item() or $this->is_category_item()) {
1326             print_error('cannotsetparentforcatoritem');
1327         }
1329         if ($this->categoryid == $parentid) {
1330             return true;
1331         }
1333         // find parent and check course id
1334         if (!$parent_category = grade_category::fetch(array('id'=>$parentid, 'courseid'=>$this->courseid))) {
1335             return false;
1336         }
1338         // MDL-19407 If moving from a non-SWM category to a SWM category, convert aggregationcoef to 0
1339         $currentparent = $this->load_parent_category();
1341         if ($currentparent->aggregation != GRADE_AGGREGATE_WEIGHTED_MEAN2 && $parent_category->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN2) {
1342             $this->aggregationcoef = 0;
1343         }
1345         $this->force_regrading();
1347         // set new parent
1348         $this->categoryid = $parent_category->id;
1349         $this->parent_category =& $parent_category;
1351         return $this->update();
1352     }
1354     /**
1355      * Makes sure value is a valid grade value.
1356      *
1357      * @param float $gradevalue
1358      * @return mixed float or int fixed grade value
1359      */
1360     public function bounded_grade($gradevalue) {
1361         global $CFG;
1363         if (is_null($gradevalue)) {
1364             return null;
1365         }
1367         if ($this->gradetype == GRADE_TYPE_SCALE) {
1368             // no >100% grades hack for scale grades!
1369             // 1.5 is rounded to 2 ;-)
1370             return (int)bounded_number($this->grademin, round($gradevalue+0.00001), $this->grademax);
1371         }
1373         $grademax = $this->grademax;
1375         // NOTE: if you change this value you must manually reset the needsupdate flag in all grade items
1376         $maxcoef = isset($CFG->gradeoverhundredprocentmax) ? $CFG->gradeoverhundredprocentmax : 10; // 1000% max by default
1378         if (!empty($CFG->unlimitedgrades)) {
1379             // NOTE: if you change this value you must manually reset the needsupdate flag in all grade items
1380             $grademax = $grademax * $maxcoef;
1381         } else if ($this->is_category_item() or $this->is_course_item()) {
1382             $category = $this->load_item_category();
1383             if ($category->aggregation >= 100) {
1384                 // grade >100% hack
1385                 $grademax = $grademax * $maxcoef;
1386             }
1387         }
1389         return (float)bounded_number($this->grademin, $gradevalue, $grademax);
1390     }
1392     /**
1393      * Finds out on which other items does this depend directly when doing calculation or category aggregation
1394      *
1395      * @param bool $reset_cache
1396      * @return array of grade_item IDs this one depends on
1397      */
1398     public function depends_on($reset_cache=false) {
1399         global $CFG, $DB;
1401         if ($reset_cache) {
1402             $this->dependson_cache = null;
1403         } else if (isset($this->dependson_cache)) {
1404             return $this->dependson_cache;
1405         }
1407         if ($this->is_locked()) {
1408             // locked items do not need to be regraded
1409             $this->dependson_cache = array();
1410             return $this->dependson_cache;
1411         }
1413         if ($this->is_calculated()) {
1414             if (preg_match_all('/##gi(\d+)##/', $this->calculation, $matches)) {
1415                 $this->dependson_cache = array_unique($matches[1]); // remove duplicates
1416                 return $this->dependson_cache;
1417             } else {
1418                 $this->dependson_cache = array();
1419                 return $this->dependson_cache;
1420             }
1422         } else if ($grade_category = $this->load_item_category()) {
1423             $params = array();
1425             //only items with numeric or scale values can be aggregated
1426             if ($this->gradetype != GRADE_TYPE_VALUE and $this->gradetype != GRADE_TYPE_SCALE) {
1427                 $this->dependson_cache = array();
1428                 return $this->dependson_cache;
1429             }
1431             $grade_category->apply_forced_settings();
1433             if (empty($CFG->enableoutcomes) or $grade_category->aggregateoutcomes) {
1434                 $outcomes_sql = "";
1435             } else {
1436                 $outcomes_sql = "AND gi.outcomeid IS NULL";
1437             }
1439             if (empty($CFG->grade_includescalesinaggregation)) {
1440                 $gtypes = "gi.gradetype = ?";
1441                 $params[] = GRADE_TYPE_VALUE;
1442             } else {
1443                 $gtypes = "(gi.gradetype = ? OR gi.gradetype = ?)";
1444                 $params[] = GRADE_TYPE_VALUE;
1445                 $params[] = GRADE_TYPE_SCALE;
1446             }
1448             if ($grade_category->aggregatesubcats) {
1449                 // return all children excluding category items
1450                 $params[] = $this->courseid;
1451                 $params[] = '%/' . $grade_category->id . '/%';
1452                 $sql = "SELECT gi.id
1453                           FROM {grade_items} gi
1454                           JOIN {grade_categories} gc ON gi.categoryid = gc.id
1455                          WHERE $gtypes
1456                                $outcomes_sql
1457                                AND gi.courseid = ?
1458                                AND gc.path LIKE ?";
1459             } else {
1460                 $params[] = $grade_category->id;
1461                 $params[] = $this->courseid;
1462                 $params[] = $grade_category->id;
1463                 $params[] = $this->courseid;
1464                 if (empty($CFG->grade_includescalesinaggregation)) {
1465                     $params[] = GRADE_TYPE_VALUE;
1466                 } else {
1467                     $params[] = GRADE_TYPE_VALUE;
1468                     $params[] = GRADE_TYPE_SCALE;
1469                 }
1470                 $sql = "SELECT gi.id
1471                           FROM {grade_items} gi
1472                          WHERE $gtypes
1473                                AND gi.categoryid = ?
1474                                AND gi.courseid = ?
1475                                $outcomes_sql
1476                         UNION
1478                         SELECT gi.id
1479                           FROM {grade_items} gi, {grade_categories} gc
1480                          WHERE (gi.itemtype = 'category' OR gi.itemtype = 'course') AND gi.iteminstance=gc.id
1481                                AND gc.parent = ?
1482                                AND gi.courseid = ?
1483                                AND $gtypes
1484                                $outcomes_sql";
1485             }
1487             if ($children = $DB->get_records_sql($sql, $params)) {
1488                 $this->dependson_cache = array_keys($children);
1489                 return $this->dependson_cache;
1490             } else {
1491                 $this->dependson_cache = array();
1492                 return $this->dependson_cache;
1493             }
1495         } else {
1496             $this->dependson_cache = array();
1497             return $this->dependson_cache;
1498         }
1499     }
1501     /**
1502      * Refetch grades from modules, plugins.
1503      *
1504      * @param int $userid optional, limit the refetch to a single user
1505      * @return bool Returns true on success or if there is nothing to do
1506      */
1507     public function refresh_grades($userid=0) {
1508         global $DB;
1509         if ($this->itemtype == 'mod') {
1510             if ($this->is_outcome_item()) {
1511                 //nothing to do
1512                 return true;
1513             }
1515             if (!$activity = $DB->get_record($this->itemmodule, array('id' => $this->iteminstance))) {
1516                 debugging("Can not find $this->itemmodule activity with id $this->iteminstance");
1517                 return false;
1518             }
1520             if (!$cm = get_coursemodule_from_instance($this->itemmodule, $activity->id, $this->courseid)) {
1521                 debugging('Can not find course module');
1522                 return false;
1523             }
1525             $activity->modname    = $this->itemmodule;
1526             $activity->cmidnumber = $cm->idnumber;
1528             return grade_update_mod_grades($activity, $userid);
1529         }
1531         return true;
1532     }
1534     /**
1535      * Updates final grade value for given user, this is a only way to update final
1536      * grades from gradebook and import because it logs the change in history table
1537      * and deals with overridden flag. This flag is set to prevent later overriding
1538      * from raw grades submitted from modules.
1539      *
1540      * @param int $userid The graded user
1541      * @param float|false $finalgrade The float value of final grade, false means do not change
1542      * @param string $source The modification source
1543      * @param string $feedback Optional teacher feedback
1544      * @param int $feedbackformat A format like FORMAT_PLAIN or FORMAT_HTML
1545      * @param int $usermodified The ID of the user making the modification
1546      * @return bool success
1547      */
1548     public function update_final_grade($userid, $finalgrade=false, $source=NULL, $feedback=false, $feedbackformat=FORMAT_MOODLE, $usermodified=null) {
1549         global $USER, $CFG;
1551         $result = true;
1553         // no grading used or locked
1554         if ($this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) {
1555             return false;
1556         }
1558         $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid));
1559         $grade->grade_item =& $this; // prevent db fetching of this grade_item
1561         if (empty($usermodified)) {
1562             $grade->usermodified = $USER->id;
1563         } else {
1564             $grade->usermodified = $usermodified;
1565         }
1567         if ($grade->is_locked()) {
1568             // do not update locked grades at all
1569             return false;
1570         }
1572         $locktime = $grade->get_locktime();
1573         if ($locktime and $locktime < time()) {
1574             // do not update grades that should be already locked, force regrade instead
1575             $this->force_regrading();
1576             return false;
1577         }
1579         $oldgrade = new stdClass();
1580         $oldgrade->finalgrade     = $grade->finalgrade;
1581         $oldgrade->overridden     = $grade->overridden;
1582         $oldgrade->feedback       = $grade->feedback;
1583         $oldgrade->feedbackformat = $grade->feedbackformat;
1585         // MDL-31713 rawgramemin and max must be up to date so conditional access %'s works properly.
1586         $grade->rawgrademin = $this->grademin;
1587         $grade->rawgrademax = $this->grademax;
1588         $grade->rawscaleid  = $this->scaleid;
1590         // changed grade?
1591         if ($finalgrade !== false) {
1592             if ($this->is_overridable_item()) {
1593                 $grade->overridden = time();
1594             }
1596             $grade->finalgrade = $this->bounded_grade($finalgrade);
1597         }
1599         // do we have comment from teacher?
1600         if ($feedback !== false) {
1601             if ($this->is_overridable_item_feedback()) {
1602                 // external items (modules, plugins) may have own feedback
1603                 $grade->overridden = time();
1604             }
1606             $grade->feedback       = $feedback;
1607             $grade->feedbackformat = $feedbackformat;
1608         }
1610         if (empty($grade->id)) {
1611             $grade->timecreated  = null;   // hack alert - date submitted - no submission yet
1612             $grade->timemodified = time(); // hack alert - date graded
1613             $result = (bool)$grade->insert($source);
1615             // If the grade insert was successful and the final grade was not null then trigger a user_graded event.
1616             if ($result && !is_null($grade->finalgrade)) {
1617                 \core\event\user_graded::create_from_grade($grade)->trigger();
1618             }
1619         } else if (grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)
1620                 or $grade->feedback       !== $oldgrade->feedback
1621                 or $grade->feedbackformat != $oldgrade->feedbackformat
1622                 or ($oldgrade->overridden == 0 and $grade->overridden > 0)) {
1623             $grade->timemodified = time(); // hack alert - date graded
1624             $result = $grade->update($source);
1626             // If the grade update was successful and the actual grade has changed then trigger a user_graded event.
1627             if ($result && grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)) {
1628                 \core\event\user_graded::create_from_grade($grade)->trigger();
1629             }
1630         } else {
1631             // no grade change
1632             return $result;
1633         }
1635         if (!$result) {
1636             // something went wrong - better force final grade recalculation
1637             $this->force_regrading();
1639         } else if ($this->is_course_item() and !$this->needsupdate) {
1640             if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
1641                 $this->force_regrading();
1642             }
1644         } else if (!$this->needsupdate) {
1645             $course_item = grade_item::fetch_course_item($this->courseid);
1646             if (!$course_item->needsupdate) {
1647                 if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
1648                     $this->force_regrading();
1649                 }
1650             } else {
1651                 $this->force_regrading();
1652             }
1653         }
1655         return $result;
1656     }
1659     /**
1660      * Updates raw grade value for given user, this is a only way to update raw
1661      * grades from external source (modules, etc.),
1662      * because it logs the change in history table and deals with final grade recalculation.
1663      *
1664      * @param int $userid the graded user
1665      * @param mixed $rawgrade float value of raw grade - false means do not change
1666      * @param string $source modification source
1667      * @param string $feedback optional teacher feedback
1668      * @param int $feedbackformat A format like FORMAT_PLAIN or FORMAT_HTML
1669      * @param int $usermodified the ID of the user who did the grading
1670      * @param int $dategraded A timestamp of when the student's work was graded
1671      * @param int $datesubmitted A timestamp of when the student's work was submitted
1672      * @param grade_grade $grade A grade object, useful for bulk upgrades
1673      * @return bool success
1674      */
1675     public function update_raw_grade($userid, $rawgrade=false, $source=NULL, $feedback=false, $feedbackformat=FORMAT_MOODLE, $usermodified=null, $dategraded=null, $datesubmitted=null, $grade=null) {
1676         global $USER;
1678         $result = true;
1680         // calculated grades can not be updated; course and category can not be updated  because they are aggregated
1681         if (!$this->is_raw_used() or $this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) {
1682             return false;
1683         }
1685         if (is_null($grade)) {
1686             //fetch from db
1687             $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid));
1688         }
1689         $grade->grade_item =& $this; // prevent db fetching of this grade_item
1691         if (empty($usermodified)) {
1692             $grade->usermodified = $USER->id;
1693         } else {
1694             $grade->usermodified = $usermodified;
1695         }
1697         if ($grade->is_locked()) {
1698             // do not update locked grades at all
1699             return false;
1700         }
1702         $locktime = $grade->get_locktime();
1703         if ($locktime and $locktime < time()) {
1704             // do not update grades that should be already locked and force regrade
1705             $this->force_regrading();
1706             return false;
1707         }
1709         $oldgrade = new stdClass();
1710         $oldgrade->finalgrade     = $grade->finalgrade;
1711         $oldgrade->rawgrade       = $grade->rawgrade;
1712         $oldgrade->rawgrademin    = $grade->rawgrademin;
1713         $oldgrade->rawgrademax    = $grade->rawgrademax;
1714         $oldgrade->rawscaleid     = $grade->rawscaleid;
1715         $oldgrade->feedback       = $grade->feedback;
1716         $oldgrade->feedbackformat = $grade->feedbackformat;
1718         // use new min and max
1719         $grade->rawgrade    = $grade->rawgrade;
1720         $grade->rawgrademin = $this->grademin;
1721         $grade->rawgrademax = $this->grademax;
1722         $grade->rawscaleid  = $this->scaleid;
1724         // change raw grade?
1725         if ($rawgrade !== false) {
1726             $grade->rawgrade = $rawgrade;
1727         }
1729         // empty feedback means no feedback at all
1730         if ($feedback === '') {
1731             $feedback = null;
1732         }
1734         // do we have comment from teacher?
1735         if ($feedback !== false and !$grade->is_overridden()) {
1736             $grade->feedback       = $feedback;
1737             $grade->feedbackformat = $feedbackformat;
1738         }
1740         // update final grade if possible
1741         if (!$grade->is_locked() and !$grade->is_overridden()) {
1742             $grade->finalgrade = $this->adjust_raw_grade($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
1743         }
1745         // TODO: hack alert - create new fields for these in 2.0
1746         $oldgrade->timecreated  = $grade->timecreated;
1747         $oldgrade->timemodified = $grade->timemodified;
1749         $grade->timecreated = $datesubmitted;
1751         if ($grade->is_overridden()) {
1752             // keep original graded date - update_final_grade() sets this for overridden grades
1754         } else if (is_null($grade->rawgrade) and is_null($grade->feedback)) {
1755             // no grade and feedback means no grading yet
1756             $grade->timemodified = null;
1758         } else if (!empty($dategraded)) {
1759             // fine - module sends info when graded (yay!)
1760             $grade->timemodified = $dategraded;
1762         } else if (grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)
1763                    or $grade->feedback !== $oldgrade->feedback) {
1764             // guess - if either grade or feedback changed set new graded date
1765             $grade->timemodified = time();
1767         } else {
1768             //keep original graded date
1769         }
1770         // end of hack alert
1772         if (empty($grade->id)) {
1773             $result = (bool)$grade->insert($source);
1775             // If the grade insert was successful and the final grade was not null then trigger a user_graded event.
1776             if ($result && !is_null($grade->finalgrade)) {
1777                 \core\event\user_graded::create_from_grade($grade)->trigger();
1778             }
1779         } else if (grade_floats_different($grade->finalgrade,  $oldgrade->finalgrade)
1780                 or grade_floats_different($grade->rawgrade,    $oldgrade->rawgrade)
1781                 or grade_floats_different($grade->rawgrademin, $oldgrade->rawgrademin)
1782                 or grade_floats_different($grade->rawgrademax, $oldgrade->rawgrademax)
1783                 or $grade->rawscaleid     != $oldgrade->rawscaleid
1784                 or $grade->feedback       !== $oldgrade->feedback
1785                 or $grade->feedbackformat != $oldgrade->feedbackformat
1786                 or $grade->timecreated    != $oldgrade->timecreated  // part of hack above
1787                 or $grade->timemodified   != $oldgrade->timemodified // part of hack above
1788                 ) {
1789             $result = $grade->update($source);
1791             // If the grade update was successful and the actual grade has changed then trigger a user_graded event.
1792             if ($result && grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)) {
1793                 \core\event\user_graded::create_from_grade($grade)->trigger();
1794             }
1795         } else {
1796             return $result;
1797         }
1799         if (!$result) {
1800             // something went wrong - better force final grade recalculation
1801             $this->force_regrading();
1803         } else if (!$this->needsupdate) {
1804             $course_item = grade_item::fetch_course_item($this->courseid);
1805             if (!$course_item->needsupdate) {
1806                 if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
1807                     $this->force_regrading();
1808                 }
1809             }
1810         }
1812         return $result;
1813     }
1815     /**
1816      * Calculates final grade values using the formula in the calculation property.
1817      * The parameters are taken from final grades of grade items in current course only.
1818      *
1819      * @param int $userid Supply a user ID to limit the calculations to the grades of a single user
1820      * @return bool false if error
1821      */
1822     public function compute($userid=null) {
1823         global $CFG, $DB;
1825         if (!$this->is_calculated()) {
1826             return false;
1827         }
1829         require_once($CFG->libdir.'/mathslib.php');
1831         if ($this->is_locked()) {
1832             return true; // no need to recalculate locked items
1833         }
1835         // Precreate grades - we need them to exist
1836         if ($userid) {
1837             $missing = array();
1838             if (!$DB->record_exists('grade_grades', array('itemid'=>$this->id, 'userid'=>$userid))) {
1839                 $m = new stdClass();
1840                 $m->userid = $userid;
1841                 $missing[] = $m;
1842             }
1843         } else {
1844             // Find any users who have grades for some but not all grade items in this course
1845             $params = array('gicourseid' => $this->courseid, 'ggitemid' => $this->id);
1846             $sql = "SELECT gg.userid
1847                       FROM {grade_grades} gg
1848                            JOIN {grade_items} gi
1849                            ON (gi.id = gg.itemid AND gi.courseid = :gicourseid)
1850                      GROUP BY gg.userid
1851                      HAVING SUM(CASE WHEN gg.itemid = :ggitemid THEN 1 ELSE 0 END) = 0";
1852             $missing = $DB->get_records_sql($sql, $params);
1853         }
1855         if ($missing) {
1856             foreach ($missing as $m) {
1857                 $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$m->userid), false);
1858                 $grade->grade_item =& $this;
1859                 $grade->insert('system');
1860             }
1861         }
1863         // get used items
1864         $useditems = $this->depends_on();
1866         // prepare formula and init maths library
1867         $formula = preg_replace('/##(gi\d+)##/', '\1', $this->calculation);
1868         if (strpos($formula, '[[') !== false) {
1869             // missing item
1870             return false;
1871         }
1872         $this->formula = new calc_formula($formula);
1874         // where to look for final grades?
1875         // this itemid is added so that we use only one query for source and final grades
1876         $gis = array_merge($useditems, array($this->id));
1877         list($usql, $params) = $DB->get_in_or_equal($gis);
1879         if ($userid) {
1880             $usersql = "AND g.userid=?";
1881             $params[] = $userid;
1882         } else {
1883             $usersql = "";
1884         }
1886         $grade_inst = new grade_grade();
1887         $fields = 'g.'.implode(',g.', $grade_inst->required_fields);
1889         $params[] = $this->courseid;
1890         $sql = "SELECT $fields
1891                   FROM {grade_grades} g, {grade_items} gi
1892                  WHERE gi.id = g.itemid AND gi.id $usql $usersql AND gi.courseid=?
1893                  ORDER BY g.userid";
1895         $return = true;
1897         // group the grades by userid and use formula on the group
1898         $rs = $DB->get_recordset_sql($sql, $params);
1899         if ($rs->valid()) {
1900             $prevuser = 0;
1901             $grade_records   = array();
1902             $oldgrade    = null;
1903             foreach ($rs as $used) {
1904                 if ($used->userid != $prevuser) {
1905                     if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {
1906                         $return = false;
1907                     }
1908                     $prevuser = $used->userid;
1909                     $grade_records   = array();
1910                     $oldgrade    = null;
1911                 }
1912                 if ($used->itemid == $this->id) {
1913                     $oldgrade = $used;
1914                 }
1915                 $grade_records['gi'.$used->itemid] = $used->finalgrade;
1916             }
1917             if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {
1918                 $return = false;
1919             }
1920         }
1921         $rs->close();
1923         return $return;
1924     }
1926     /**
1927      * Internal function that does the final grade calculation
1928      *
1929      * @param int $userid The user ID
1930      * @param array $params An array of grade items of the form {'gi'.$itemid]} => $finalgrade
1931      * @param array $useditems An array of grade item IDs that this grade item depends on plus its own ID
1932      * @param grade_grade $oldgrade A grade_grade instance containing the old values from the database
1933      * @return bool False if an error occurred
1934      */
1935     public function use_formula($userid, $params, $useditems, $oldgrade) {
1936         if (empty($userid)) {
1937             return true;
1938         }
1940         // add missing final grade values
1941         // not graded (null) is counted as 0 - the spreadsheet way
1942         $allinputsnull = true;
1943         foreach($useditems as $gi) {
1944             if (!array_key_exists('gi'.$gi, $params) || is_null($params['gi'.$gi])) {
1945                 $params['gi'.$gi] = 0;
1946             } else {
1947                 $params['gi'.$gi] = (float)$params['gi'.$gi];
1948                 if ($gi != $this->id) {
1949                     $allinputsnull = false;
1950                 }
1951             }
1952         }
1954         // can not use own final grade during calculation
1955         unset($params['gi'.$this->id]);
1957         // insert final grade - will be needed later anyway
1958         if ($oldgrade) {
1959             $oldfinalgrade = $oldgrade->finalgrade;
1960             $grade = new grade_grade($oldgrade, false); // fetching from db is not needed
1961             $grade->grade_item =& $this;
1963         } else {
1964             $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid), false);
1965             $grade->grade_item =& $this;
1966             $grade->insert('system');
1967             $oldfinalgrade = null;
1968         }
1970         // no need to recalculate locked or overridden grades
1971         if ($grade->is_locked() or $grade->is_overridden()) {
1972             return true;
1973         }
1975         if ($allinputsnull) {
1976             $grade->finalgrade = null;
1977             $result = true;
1979         } else {
1981             // do the calculation
1982             $this->formula->set_params($params);
1983             $result = $this->formula->evaluate();
1985             if ($result === false) {
1986                 $grade->finalgrade = null;
1988             } else {
1989                 // normalize
1990                 $grade->finalgrade = $this->bounded_grade($result);
1991             }
1993         }
1995         // update in db if changed
1996         if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) {
1997             $grade->timemodified = time();
1998             $success = $grade->update('compute');
2000             // If successful trigger a user_graded event.
2001             if ($success) {
2002                 \core\event\user_graded::create_from_grade($grade)->trigger();
2003             }
2004         }
2006         if ($result !== false) {
2007             //lock grade if needed
2008         }
2010         if ($result === false) {
2011             return false;
2012         } else {
2013             return true;
2014         }
2016     }
2018     /**
2019      * Validate the formula.
2020      *
2021      * @param string $formulastr
2022      * @return bool true if calculation possible, false otherwise
2023      */
2024     public function validate_formula($formulastr) {
2025         global $CFG, $DB;
2026         require_once($CFG->libdir.'/mathslib.php');
2028         $formulastr = grade_item::normalize_formula($formulastr, $this->courseid);
2030         if (empty($formulastr)) {
2031             return true;
2032         }
2034         if (strpos($formulastr, '=') !== 0) {
2035             return get_string('errorcalculationnoequal', 'grades');
2036         }
2038         // get used items
2039         if (preg_match_all('/##gi(\d+)##/', $formulastr, $matches)) {
2040             $useditems = array_unique($matches[1]); // remove duplicates
2041         } else {
2042             $useditems = array();
2043         }
2045         // MDL-11902
2046         // unset the value if formula is trying to reference to itself
2047         // but array keys does not match itemid
2048         if (!empty($this->id)) {
2049             $useditems = array_diff($useditems, array($this->id));
2050             //unset($useditems[$this->id]);
2051         }
2053         // prepare formula and init maths library
2054         $formula = preg_replace('/##(gi\d+)##/', '\1', $formulastr);
2055         $formula = new calc_formula($formula);
2058         if (empty($useditems)) {
2059             $grade_items = array();
2061         } else {
2062             list($usql, $params) = $DB->get_in_or_equal($useditems);
2063             $params[] = $this->courseid;
2064             $sql = "SELECT gi.*
2065                       FROM {grade_items} gi
2066                      WHERE gi.id $usql and gi.courseid=?"; // from the same course only!
2068             if (!$grade_items = $DB->get_records_sql($sql, $params)) {
2069                 $grade_items = array();
2070             }
2071         }
2073         $params = array();
2074         foreach ($useditems as $itemid) {
2075             // make sure all grade items exist in this course
2076             if (!array_key_exists($itemid, $grade_items)) {
2077                 return false;
2078             }
2079             // use max grade when testing formula, this should be ok in 99.9%
2080             // division by 0 is one of possible problems
2081             $params['gi'.$grade_items[$itemid]->id] = $grade_items[$itemid]->grademax;
2082         }
2084         // do the calculation
2085         $formula->set_params($params);
2086         $result = $formula->evaluate();
2088         // false as result indicates some problem
2089         if ($result === false) {
2090             // TODO: add more error hints
2091             return get_string('errorcalculationunknown', 'grades');
2092         } else {
2093             return true;
2094         }
2095     }
2097     /**
2098      * Returns the value of the display type
2099      *
2100      * It can be set at 3 levels: grade_item, course setting and site. The lowest level overrides the higher ones.
2101      *
2102      * @return int Display type
2103      */
2104     public function get_displaytype() {
2105         global $CFG;
2107         if ($this->display == GRADE_DISPLAY_TYPE_DEFAULT) {
2108             return grade_get_setting($this->courseid, 'displaytype', $CFG->grade_displaytype);
2110         } else {
2111             return $this->display;
2112         }
2113     }
2115     /**
2116      * Returns the value of the decimals field
2117      *
2118      * It can be set at 3 levels: grade_item, course setting and site. The lowest level overrides the higher ones.
2119      *
2120      * @return int Decimals (0 - 5)
2121      */
2122     public function get_decimals() {
2123         global $CFG;
2125         if (is_null($this->decimals)) {
2126             return grade_get_setting($this->courseid, 'decimalpoints', $CFG->grade_decimalpoints);
2128         } else {
2129             return $this->decimals;
2130         }
2131     }
2133     /**
2134      * Returns a string representing the range of grademin - grademax for this grade item.
2135      *
2136      * @param int $rangesdisplaytype
2137      * @param int $rangesdecimalpoints
2138      * @return string
2139      */
2140     function get_formatted_range($rangesdisplaytype=null, $rangesdecimalpoints=null) {
2142         global $USER;
2144         // Determine which display type to use for this average
2145         if (isset($USER->gradeediting) && array_key_exists($this->courseid, $USER->gradeediting) && $USER->gradeediting[$this->courseid]) {
2146             $displaytype = GRADE_DISPLAY_TYPE_REAL;
2148         } else if ($rangesdisplaytype == GRADE_REPORT_PREFERENCE_INHERIT) { // no ==0 here, please resave report and user prefs
2149             $displaytype = $this->get_displaytype();
2151         } else {
2152             $displaytype = $rangesdisplaytype;
2153         }
2155         // Override grade_item setting if a display preference (not default) was set for the averages
2156         if ($rangesdecimalpoints == GRADE_REPORT_PREFERENCE_INHERIT) {
2157             $decimalpoints = $this->get_decimals();
2159         } else {
2160             $decimalpoints = $rangesdecimalpoints;
2161         }
2163         if ($displaytype == GRADE_DISPLAY_TYPE_PERCENTAGE) {
2164             $grademin = "0 %";
2165             $grademax = "100 %";
2167         } else {
2168             $grademin = grade_format_gradevalue($this->grademin, $this, true, $displaytype, $decimalpoints);
2169             $grademax = grade_format_gradevalue($this->grademax, $this, true, $displaytype, $decimalpoints);
2170         }
2172         return $grademin.'&ndash;'. $grademax;
2173     }
2175     /**
2176      * Queries parent categories recursively to find the aggregationcoef type that applies to this grade item.
2177      *
2178      * @return string|false Returns the coefficient string of false is no coefficient is being used
2179      */
2180     public function get_coefstring() {
2181         $parent_category = $this->load_parent_category();
2182         if ($this->is_category_item()) {
2183             $parent_category = $parent_category->load_parent_category();
2184         }
2186         if ($parent_category->is_aggregationcoef_used()) {
2187             return $parent_category->get_coefstring();
2188         } else {
2189             return false;
2190         }
2191     }
2193     /**
2194      * Returns whether the grade item can control the visibility of the grades
2195      *
2196      * @return bool
2197      */
2198     public function can_control_visibility() {
2199         if (core_component::get_plugin_directory($this->itemtype, $this->itemmodule)) {
2200             return !plugin_supports($this->itemtype, $this->itemmodule, FEATURE_CONTROLS_GRADE_VISIBILITY, false);
2201         }
2202         return parent::can_control_visibility();
2203     }
2205     /**
2206      * Used to notify the completion system (if necessary) that a user's grade
2207      * has changed, and clear up a possible score cache.
2208      *
2209      * @param bool $deleted True if grade was actually deleted
2210      */
2211     protected function notify_changed($deleted) {
2212         global $CFG;
2214         // Condition code may cache the grades for conditional availability of
2215         // modules or sections. (This code should use a hook for communication
2216         // with plugin, but hooks are not implemented at time of writing.)
2217         if (!empty($CFG->enableavailability) && class_exists('\availability_grade\callbacks')) {
2218             \availability_grade\callbacks::grade_item_changed($this->courseid);
2219         }
2220     }