MDL-67062 core_h5p: delete libraries
[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      * Constructor. Optionally (and by default) attempts to fetch corresponding row from the database
262      *
263      * @param array $params An array with required parameters for this grade object.
264      * @param bool $fetch Whether to fetch corresponding row from the database or not,
265      *        optional fields might not be defined if false used
266      */
267     public function __construct($params = null, $fetch = true) {
268         global $CFG;
269         // Set grademax from $CFG->gradepointdefault .
270         self::set_properties($this, array('grademax' => $CFG->gradepointdefault));
271         parent::__construct($params, $fetch);
272     }
274     /**
275      * In addition to update() as defined in grade_object, handle the grade_outcome and grade_scale objects.
276      * Force regrading if necessary, rounds the float numbers using php function,
277      * the reason is we need to compare the db value with computed number to skip regrading if possible.
278      *
279      * @param string $source from where was the object inserted (mod/forum, manual, etc.)
280      * @return bool success
281      */
282     public function update($source=null) {
283         // reset caches
284         $this->dependson_cache = null;
286         // Retrieve scale and infer grademax/min from it if needed
287         $this->load_scale();
289         // make sure there is not 0 in outcomeid
290         if (empty($this->outcomeid)) {
291             $this->outcomeid = null;
292         }
294         if ($this->qualifies_for_regrading()) {
295             $this->force_regrading();
296         }
298         $this->timemodified = time();
300         $this->grademin        = grade_floatval($this->grademin);
301         $this->grademax        = grade_floatval($this->grademax);
302         $this->multfactor      = grade_floatval($this->multfactor);
303         $this->plusfactor      = grade_floatval($this->plusfactor);
304         $this->aggregationcoef = grade_floatval($this->aggregationcoef);
305         $this->aggregationcoef2 = grade_floatval($this->aggregationcoef2);
307         $result = parent::update($source);
309         if ($result) {
310             $event = \core\event\grade_item_updated::create_from_grade_item($this);
311             $event->trigger();
312         }
314         return $result;
315     }
317     /**
318      * Compares the values held by this object with those of the matching record in DB, and returns
319      * whether or not these differences are sufficient to justify an update of all parent objects.
320      * This assumes that this object has an id number and a matching record in DB. If not, it will return false.
321      *
322      * @return bool
323      */
324     public function qualifies_for_regrading() {
325         if (empty($this->id)) {
326             return false;
327         }
329         $db_item = new grade_item(array('id' => $this->id));
331         $calculationdiff = $db_item->calculation != $this->calculation;
332         $categorydiff    = $db_item->categoryid  != $this->categoryid;
333         $gradetypediff   = $db_item->gradetype   != $this->gradetype;
334         $scaleiddiff     = $db_item->scaleid     != $this->scaleid;
335         $outcomeiddiff   = $db_item->outcomeid   != $this->outcomeid;
336         $locktimediff    = $db_item->locktime    != $this->locktime;
337         $grademindiff    = grade_floats_different($db_item->grademin,        $this->grademin);
338         $grademaxdiff    = grade_floats_different($db_item->grademax,        $this->grademax);
339         $multfactordiff  = grade_floats_different($db_item->multfactor,      $this->multfactor);
340         $plusfactordiff  = grade_floats_different($db_item->plusfactor,      $this->plusfactor);
341         $acoefdiff       = grade_floats_different($db_item->aggregationcoef, $this->aggregationcoef);
342         $acoefdiff2      = grade_floats_different($db_item->aggregationcoef2, $this->aggregationcoef2);
343         $weightoverride  = grade_floats_different($db_item->weightoverride, $this->weightoverride);
345         $needsupdatediff = !$db_item->needsupdate &&  $this->needsupdate;    // force regrading only if setting the flag first time
346         $lockeddiff      = !empty($db_item->locked) && empty($this->locked); // force regrading only when unlocking
348         return ($calculationdiff || $categorydiff || $gradetypediff || $grademaxdiff || $grademindiff || $scaleiddiff
349              || $outcomeiddiff || $multfactordiff || $plusfactordiff || $needsupdatediff
350              || $lockeddiff || $acoefdiff || $acoefdiff2 || $weightoverride || $locktimediff);
351     }
353     /**
354      * Finds and returns a grade_item instance based on params.
355      *
356      * @static
357      * @param array $params associative arrays varname=>value
358      * @return grade_item|bool Returns a grade_item instance or false if none found
359      */
360     public static function fetch($params) {
361         return grade_object::fetch_helper('grade_items', 'grade_item', $params);
362     }
364     /**
365      * Check to see if there are any existing grades for this grade_item.
366      *
367      * @return boolean - true if there are valid grades for this grade_item.
368      */
369     public function has_grades() {
370         global $DB;
372         $count = $DB->count_records_select('grade_grades',
373                                            'itemid = :gradeitemid AND finalgrade IS NOT NULL',
374                                            array('gradeitemid' => $this->id));
375         return $count > 0;
376     }
378     /**
379      * Check to see if there are existing overridden grades for this grade_item.
380      *
381      * @return boolean - true if there are overridden grades for this grade_item.
382      */
383     public function has_overridden_grades() {
384         global $DB;
386         $count = $DB->count_records_select('grade_grades',
387                                            'itemid = :gradeitemid AND finalgrade IS NOT NULL AND overridden > 0',
388                                            array('gradeitemid' => $this->id));
389         return $count > 0;
390     }
392     /**
393      * Finds and returns all grade_item instances based on params.
394      *
395      * @static
396      * @param array $params associative arrays varname=>value
397      * @return array array of grade_item instances or false if none found.
398      */
399     public static function fetch_all($params) {
400         return grade_object::fetch_all_helper('grade_items', 'grade_item', $params);
401     }
403     /**
404      * Delete all grades and force_regrading of parent category.
405      *
406      * @param string $source from where was the object deleted (mod/forum, manual, etc.)
407      * @return bool success
408      */
409     public function delete($source=null) {
410         $this->delete_all_grades($source);
411         return parent::delete($source);
412     }
414     /**
415      * Delete all grades
416      *
417      * @param string $source from where was the object deleted (mod/forum, manual, etc.)
418      * @return bool
419      */
420     public function delete_all_grades($source=null) {
421         if (!$this->is_course_item()) {
422             $this->force_regrading();
423         }
425         if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
426             foreach ($grades as $grade) {
427                 $grade->delete($source);
428             }
429         }
431         // Delete all the historical files.
432         // We only support feedback files for modules atm.
433         if ($this->is_external_item()) {
434             $fs = new file_storage();
435             $fs->delete_area_files($this->get_context()->id, GRADE_FILE_COMPONENT, GRADE_HISTORY_FEEDBACK_FILEAREA);
436         }
438         return true;
439     }
441     /**
442      * In addition to perform parent::insert(), calls force_regrading() method too.
443      *
444      * @param string $source From where was the object inserted (mod/forum, manual, etc.)
445      * @return int PK ID if successful, false otherwise
446      */
447     public function insert($source=null) {
448         global $CFG, $DB;
450         if (empty($this->courseid)) {
451             print_error('cannotinsertgrade');
452         }
454         // load scale if needed
455         $this->load_scale();
457         // add parent category if needed
458         if (empty($this->categoryid) and !$this->is_course_item() and !$this->is_category_item()) {
459             $course_category = grade_category::fetch_course_category($this->courseid);
460             $this->categoryid = $course_category->id;
462         }
464         // always place the new items at the end, move them after insert if needed
465         $last_sortorder = $DB->get_field_select('grade_items', 'MAX(sortorder)', "courseid = ?", array($this->courseid));
466         if (!empty($last_sortorder)) {
467             $this->sortorder = $last_sortorder + 1;
468         } else {
469             $this->sortorder = 1;
470         }
472         // add proper item numbers to manual items
473         if ($this->itemtype == 'manual') {
474             if (empty($this->itemnumber)) {
475                 $this->itemnumber = 0;
476             }
477         }
479         // make sure there is not 0 in outcomeid
480         if (empty($this->outcomeid)) {
481             $this->outcomeid = null;
482         }
484         $this->timecreated = $this->timemodified = time();
486         if (parent::insert($source)) {
487             // force regrading of items if needed
488             $this->force_regrading();
490             $event = \core\event\grade_item_created::create_from_grade_item($this);
491             $event->trigger();
493             return $this->id;
495         } else {
496             debugging("Could not insert this grade_item in the database!");
497             return false;
498         }
499     }
501     /**
502      * Set idnumber of grade item, updates also course_modules table
503      *
504      * @param string $idnumber (without magic quotes)
505      * @return bool success
506      */
507     public function add_idnumber($idnumber) {
508         global $DB;
509         if (!empty($this->idnumber)) {
510             return false;
511         }
513         if ($this->itemtype == 'mod' and !$this->is_outcome_item()) {
514             if ($this->itemnumber == 0) {
515                 // for activity modules, itemnumber 0 is synced with the course_modules
516                 if (!$cm = get_coursemodule_from_instance($this->itemmodule, $this->iteminstance, $this->courseid)) {
517                     return false;
518                 }
519                 if (!empty($cm->idnumber)) {
520                     return false;
521                 }
522                 $DB->set_field('course_modules', 'idnumber', $idnumber, array('id' => $cm->id));
523                 $this->idnumber = $idnumber;
524                 return $this->update();
525             } else {
526                 $this->idnumber = $idnumber;
527                 return $this->update();
528             }
530         } else {
531             $this->idnumber = $idnumber;
532             return $this->update();
533         }
534     }
536     /**
537      * Returns the locked state of this grade_item (if the grade_item is locked OR no specific
538      * $userid is given) or the locked state of a specific grade within this item if a specific
539      * $userid is given and the grade_item is unlocked.
540      *
541      * @param int $userid The user's ID
542      * @return bool Locked state
543      */
544     public function is_locked($userid=NULL) {
545         global $CFG;
547         // Override for any grade items belonging to activities which are in the process of being deleted.
548         require_once($CFG->dirroot . '/course/lib.php');
549         if (course_module_instance_pending_deletion($this->courseid, $this->itemmodule, $this->iteminstance)) {
550             return true;
551         }
553         if (!empty($this->locked)) {
554             return true;
555         }
557         if (!empty($userid)) {
558             if ($grade = grade_grade::fetch(array('itemid'=>$this->id, 'userid'=>$userid))) {
559                 $grade->grade_item =& $this; // prevent db fetching of cached grade_item
560                 return $grade->is_locked();
561             }
562         }
564         return false;
565     }
567     /**
568      * Locks or unlocks this grade_item and (optionally) all its associated final grades.
569      *
570      * @param int $lockedstate 0, 1 or a timestamp int(10) after which date the item will be locked.
571      * @param bool $cascade Lock/unlock child objects too
572      * @param bool $refresh Refresh grades when unlocking
573      * @return bool True if grade_item all grades updated, false if at least one update fails
574      */
575     public function set_locked($lockedstate, $cascade=false, $refresh=true) {
576         if ($lockedstate) {
577         /// setting lock
578             if ($this->needsupdate) {
579                 return false; // can not lock grade without first having final grade
580             }
582             $this->locked = time();
583             $this->update();
585             if ($cascade) {
586                 $grades = $this->get_final();
587                 foreach($grades as $g) {
588                     $grade = new grade_grade($g, false);
589                     $grade->grade_item =& $this;
590                     $grade->set_locked(1, null, false);
591                 }
592             }
594             return true;
596         } else {
597         /// removing lock
598             if (!empty($this->locked) and $this->locktime < time()) {
599                 //we have to reset locktime or else it would lock up again
600                 $this->locktime = 0;
601             }
603             $this->locked = 0;
604             $this->update();
606             if ($cascade) {
607                 if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
608                     foreach($grades as $grade) {
609                         $grade->grade_item =& $this;
610                         $grade->set_locked(0, null, false);
611                     }
612                 }
613             }
615             if ($refresh) {
616                 //refresh when unlocking
617                 $this->refresh_grades();
618             }
620             return true;
621         }
622     }
624     /**
625      * Lock the grade if needed. Make sure this is called only when final grades are valid
626      */
627     public function check_locktime() {
628         if (!empty($this->locked)) {
629             return; // already locked
630         }
632         if ($this->locktime and $this->locktime < time()) {
633             $this->locked = time();
634             $this->update('locktime');
635         }
636     }
638     /**
639      * Set the locktime for this grade item.
640      *
641      * @param int $locktime timestamp for lock to activate
642      * @return void
643      */
644     public function set_locktime($locktime) {
645         $this->locktime = $locktime;
646         $this->update();
647     }
649     /**
650      * Set the locktime for this grade item.
651      *
652      * @return int $locktime timestamp for lock to activate
653      */
654     public function get_locktime() {
655         return $this->locktime;
656     }
658     /**
659      * Set the hidden status of grade_item and all grades.
660      *
661      * 0 mean always visible, 1 means always hidden and a number > 1 is a timestamp to hide until
662      *
663      * @param int $hidden new hidden status
664      * @param bool $cascade apply to child objects too
665      */
666     public function set_hidden($hidden, $cascade=false) {
667         parent::set_hidden($hidden, $cascade);
669         if ($cascade) {
670             if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
671                 foreach($grades as $grade) {
672                     $grade->grade_item =& $this;
673                     $grade->set_hidden($hidden, $cascade);
674                 }
675             }
676         }
678         //if marking item visible make sure category is visible MDL-21367
679         if( !$hidden ) {
680             $category_array = grade_category::fetch_all(array('id'=>$this->categoryid));
681             if ($category_array && array_key_exists($this->categoryid, $category_array)) {
682                 $category = $category_array[$this->categoryid];
683                 //call set_hidden on the category regardless of whether it is hidden as its parent might be hidden
684                 $category->set_hidden($hidden, false);
685             }
686         }
687     }
689     /**
690      * Returns the number of grades that are hidden
691      *
692      * @param string $groupsql SQL to limit the query by group
693      * @param array $params SQL params for $groupsql
694      * @param string $groupwheresql Where conditions for $groupsql
695      * @return int The number of hidden grades
696      */
697     public function has_hidden_grades($groupsql="", array $params=null, $groupwheresql="") {
698         global $DB;
699         $params = (array)$params;
700         $params['itemid'] = $this->id;
702         return $DB->get_field_sql("SELECT COUNT(*) FROM {grade_grades} g LEFT JOIN "
703                             ."{user} u ON g.userid = u.id $groupsql WHERE itemid = :itemid AND hidden = 1 $groupwheresql", $params);
704     }
706     /**
707      * Mark regrading as finished successfully. This will also be called when subsequent regrading will not change any grades.
708      * Situations such as an error being found will still result in the regrading being finished.
709      */
710     public function regrading_finished() {
711         global $DB;
712         $this->needsupdate = 0;
713         //do not use $this->update() because we do not want this logged in grade_item_history
714         $DB->set_field('grade_items', 'needsupdate', 0, array('id' => $this->id));
715     }
717     /**
718      * Performs the necessary calculations on the grades_final referenced by this grade_item.
719      * Also resets the needsupdate flag once successfully performed.
720      *
721      * This function must be used ONLY from lib/gradeslib.php/grade_regrade_final_grades(),
722      * because the regrading must be done in correct order!!
723      *
724      * @param int $userid Supply a user ID to limit the regrading to a single user
725      * @return bool true if ok, error string otherwise
726      */
727     public function regrade_final_grades($userid=null) {
728         global $CFG, $DB;
730         // locked grade items already have correct final grades
731         if ($this->is_locked()) {
732             return true;
733         }
735         // calculation produces final value using formula from other final values
736         if ($this->is_calculated()) {
737             if ($this->compute($userid)) {
738                 return true;
739             } else {
740                 return "Could not calculate grades for grade item"; // TODO: improve and localize
741             }
743         // noncalculated outcomes already have final values - raw grades not used
744         } else if ($this->is_outcome_item()) {
745             return true;
747         // aggregate the category grade
748         } else if ($this->is_category_item() or $this->is_course_item()) {
749             // aggregate category grade item
750             $category = $this->load_item_category();
751             $category->grade_item =& $this;
752             if ($category->generate_grades($userid)) {
753                 return true;
754             } else {
755                 return "Could not aggregate final grades for category:".$this->id; // TODO: improve and localize
756             }
758         } else if ($this->is_manual_item()) {
759             // manual items track only final grades, no raw grades
760             return true;
762         } else if (!$this->is_raw_used()) {
763             // hmm - raw grades are not used- nothing to regrade
764             return true;
765         }
767         // normal grade item - just new final grades
768         $result = true;
769         $grade_inst = new grade_grade();
770         $fields = implode(',', $grade_inst->required_fields);
771         if ($userid) {
772             $params = array($this->id, $userid);
773             $rs = $DB->get_recordset_select('grade_grades', "itemid=? AND userid=?", $params, '', $fields);
774         } else {
775             $rs = $DB->get_recordset('grade_grades', array('itemid' => $this->id), '', $fields);
776         }
777         if ($rs) {
778             foreach ($rs as $grade_record) {
779                 $grade = new grade_grade($grade_record, false);
781                 if (!empty($grade_record->locked) or !empty($grade_record->overridden)) {
782                     // this grade is locked - final grade must be ok
783                     continue;
784                 }
786                 $grade->finalgrade = $this->adjust_raw_grade($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
788                 if (grade_floats_different($grade_record->finalgrade, $grade->finalgrade)) {
789                     $success = $grade->update('system');
791                     // If successful trigger a user_graded event.
792                     if ($success) {
793                         $grade->load_grade_item();
794                         \core\event\user_graded::create_from_grade($grade, \core\event\base::USER_OTHER)->trigger();
795                     } else {
796                         $result = "Internal error updating final grade";
797                     }
798                 }
799             }
800             $rs->close();
801         }
803         return $result;
804     }
806     /**
807      * Given a float grade value or integer grade scale, applies a number of adjustment based on
808      * grade_item variables and returns the result.
809      *
810      * @param float $rawgrade The raw grade value
811      * @param float $rawmin original rawmin
812      * @param float $rawmax original rawmax
813      * @return mixed
814      */
815     public function adjust_raw_grade($rawgrade, $rawmin, $rawmax) {
816         if (is_null($rawgrade)) {
817             return null;
818         }
820         if ($this->gradetype == GRADE_TYPE_VALUE) { // Dealing with numerical grade
822             if ($this->grademax < $this->grademin) {
823                 return null;
824             }
826             if ($this->grademax == $this->grademin) {
827                 return $this->grademax; // no range
828             }
830             // Standardise score to the new grade range
831             // NOTE: skip if the activity provides a manual rescaling option.
832             $manuallyrescale = (component_callback_exists('mod_' . $this->itemmodule, 'rescale_activity_grades') !== false);
833             if (!$manuallyrescale && ($rawmin != $this->grademin or $rawmax != $this->grademax)) {
834                 $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax);
835             }
837             // Apply other grade_item factors
838             $rawgrade *= $this->multfactor;
839             $rawgrade += $this->plusfactor;
841             return $this->bounded_grade($rawgrade);
843         } else if ($this->gradetype == GRADE_TYPE_SCALE) { // Dealing with a scale value
844             if (empty($this->scale)) {
845                 $this->load_scale();
846             }
848             if ($this->grademax < 0) {
849                 return null; // scale not present - no grade
850             }
852             if ($this->grademax == 0) {
853                 return $this->grademax; // only one option
854             }
856             // Convert scale if needed
857             // NOTE: skip if the activity provides a manual rescaling option.
858             $manuallyrescale = (component_callback_exists('mod_' . $this->itemmodule, 'rescale_activity_grades') !== false);
859             if (!$manuallyrescale && ($rawmin != $this->grademin or $rawmax != $this->grademax)) {
860                 // This should never happen because scales are locked if they are in use.
861                 $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax);
862             }
864             return $this->bounded_grade($rawgrade);
867         } else if ($this->gradetype == GRADE_TYPE_TEXT or $this->gradetype == GRADE_TYPE_NONE) { // no value
868             // somebody changed the grading type when grades already existed
869             return null;
871         } else {
872             debugging("Unknown grade type");
873             return null;
874         }
875     }
877     /**
878      * Update the rawgrademax and rawgrademin for all grade_grades records for this item.
879      * Scale every rawgrade to maintain the percentage. This function should be called
880      * after the gradeitem has been updated to the new min and max values.
881      *
882      * @param float $oldgrademin The previous grade min value
883      * @param float $oldgrademax The previous grade max value
884      * @param float $newgrademin The new grade min value
885      * @param float $newgrademax The new grade max value
886      * @param string $source from where was the object inserted (mod/forum, manual, etc.)
887      * @return bool True on success
888      */
889     public function rescale_grades_keep_percentage($oldgrademin, $oldgrademax, $newgrademin, $newgrademax, $source = null) {
890         global $DB;
892         if (empty($this->id)) {
893             return false;
894         }
896         if ($oldgrademax <= $oldgrademin) {
897             // Grades cannot be scaled.
898             return false;
899         }
900         $scale = ($newgrademax - $newgrademin) / ($oldgrademax - $oldgrademin);
901         if (($newgrademax - $newgrademin) <= 1) {
902             // We would lose too much precision, lets bail.
903             return false;
904         }
906         $rs = $DB->get_recordset('grade_grades', array('itemid' => $this->id));
908         foreach ($rs as $graderecord) {
909             // For each record, create an object to work on.
910             $grade = new grade_grade($graderecord, false);
911             // Set this object in the item so it doesn't re-fetch it.
912             $grade->grade_item = $this;
914             if (!$this->is_category_item() || ($this->is_category_item() && $grade->is_overridden())) {
915                 // Updating the raw grade automatically updates the min/max.
916                 if ($this->is_raw_used()) {
917                     $rawgrade = (($grade->rawgrade - $oldgrademin) * $scale) + $newgrademin;
918                     $this->update_raw_grade(false, $rawgrade, $source, false, FORMAT_MOODLE, null, null, null, $grade);
919                 } else {
920                     $finalgrade = (($grade->finalgrade - $oldgrademin) * $scale) + $newgrademin;
921                     $this->update_final_grade($grade->userid, $finalgrade, $source);
922                 }
923             }
924         }
925         $rs->close();
927         // Mark this item for regrading.
928         $this->force_regrading();
930         return true;
931     }
933     /**
934      * Sets this grade_item's needsupdate to true. Also marks the course item as needing update.
935      *
936      * @return void
937      */
938     public function force_regrading() {
939         global $DB;
940         $this->needsupdate = 1;
941         //mark this item and course item only - categories and calculated items are always regraded
942         $wheresql = "(itemtype='course' OR id=?) AND courseid=?";
943         $params   = array($this->id, $this->courseid);
944         $DB->set_field_select('grade_items', 'needsupdate', 1, $wheresql, $params);
945     }
947     /**
948      * Instantiates a grade_scale object from the DB if this item's scaleid variable is set
949      *
950      * @return grade_scale Returns a grade_scale object or null if no scale used
951      */
952     public function load_scale() {
953         if ($this->gradetype != GRADE_TYPE_SCALE) {
954             $this->scaleid = null;
955         }
957         if (!empty($this->scaleid)) {
958             //do not load scale if already present
959             if (empty($this->scale->id) or $this->scale->id != $this->scaleid) {
960                 $this->scale = grade_scale::fetch(array('id'=>$this->scaleid));
961                 if (!$this->scale) {
962                     debugging('Incorrect scale id: '.$this->scaleid);
963                     $this->scale = null;
964                     return null;
965                 }
966                 $this->scale->load_items();
967             }
969             // Until scales are uniformly set to min=0 max=count(scaleitems)-1 throughout Moodle, we
970             // stay with the current min=1 max=count(scaleitems)
971             $this->grademax = count($this->scale->scale_items);
972             $this->grademin = 1;
974         } else {
975             $this->scale = null;
976         }
978         return $this->scale;
979     }
981     /**
982      * Instantiates a grade_outcome object from the DB if this item's outcomeid variable is set
983      *
984      * @return grade_outcome This grade item's associated grade_outcome or null
985      */
986     public function load_outcome() {
987         if (!empty($this->outcomeid)) {
988             $this->outcome = grade_outcome::fetch(array('id'=>$this->outcomeid));
989         }
990         return $this->outcome;
991     }
993     /**
994      * Returns the grade_category object this grade_item belongs to (referenced by categoryid)
995      * or category attached to category item.
996      *
997      * @return grade_category|bool Returns a grade_category object if applicable or false if this is a course item
998      */
999     public function get_parent_category() {
1000         if ($this->is_category_item() or $this->is_course_item()) {
1001             return $this->get_item_category();
1003         } else {
1004             return grade_category::fetch(array('id'=>$this->categoryid));
1005         }
1006     }
1008     /**
1009      * Calls upon the get_parent_category method to retrieve the grade_category object
1010      * from the DB and assigns it to $this->parent_category. It also returns the object.
1011      *
1012      * @return grade_category This grade item's parent grade_category.
1013      */
1014     public function load_parent_category() {
1015         if (empty($this->parent_category->id)) {
1016             $this->parent_category = $this->get_parent_category();
1017         }
1018         return $this->parent_category;
1019     }
1021     /**
1022      * Returns the grade_category for a grade category grade item
1023      *
1024      * @return grade_category|bool Returns a grade_category instance if applicable or false otherwise
1025      */
1026     public function get_item_category() {
1027         if (!$this->is_course_item() and !$this->is_category_item()) {
1028             return false;
1029         }
1030         return grade_category::fetch(array('id'=>$this->iteminstance));
1031     }
1033     /**
1034      * Calls upon the get_item_category method to retrieve the grade_category object
1035      * from the DB and assigns it to $this->item_category. It also returns the object.
1036      *
1037      * @return grade_category
1038      */
1039     public function load_item_category() {
1040         if (empty($this->item_category->id)) {
1041             $this->item_category = $this->get_item_category();
1042         }
1043         return $this->item_category;
1044     }
1046     /**
1047      * Is the grade item associated with category?
1048      *
1049      * @return bool
1050      */
1051     public function is_category_item() {
1052         return ($this->itemtype == 'category');
1053     }
1055     /**
1056      * Is the grade item associated with course?
1057      *
1058      * @return bool
1059      */
1060     public function is_course_item() {
1061         return ($this->itemtype == 'course');
1062     }
1064     /**
1065      * Is this a manually graded item?
1066      *
1067      * @return bool
1068      */
1069     public function is_manual_item() {
1070         return ($this->itemtype == 'manual');
1071     }
1073     /**
1074      * Is this an outcome item?
1075      *
1076      * @return bool
1077      */
1078     public function is_outcome_item() {
1079         return !empty($this->outcomeid);
1080     }
1082     /**
1083      * Is the grade item external - associated with module, plugin or something else?
1084      *
1085      * @return bool
1086      */
1087     public function is_external_item() {
1088         return ($this->itemtype == 'mod');
1089     }
1091     /**
1092      * Is the grade item overridable
1093      *
1094      * @return bool
1095      */
1096     public function is_overridable_item() {
1097         if ($this->is_course_item() or $this->is_category_item()) {
1098             $overridable = (bool) get_config('moodle', 'grade_overridecat');
1099         } else {
1100             $overridable = false;
1101         }
1103         return !$this->is_outcome_item() and ($this->is_external_item() or $this->is_calculated() or $overridable);
1104     }
1106     /**
1107      * Is the grade item feedback overridable
1108      *
1109      * @return bool
1110      */
1111     public function is_overridable_item_feedback() {
1112         return !$this->is_outcome_item() and $this->is_external_item();
1113     }
1115     /**
1116      * Returns true if grade items uses raw grades
1117      *
1118      * @return bool
1119      */
1120     public function is_raw_used() {
1121         return ($this->is_external_item() and !$this->is_calculated() and !$this->is_outcome_item());
1122     }
1124     /**
1125      * Returns true if the grade item is an aggreggated type grade.
1126      *
1127      * @since  Moodle 2.8.7, 2.9.1
1128      * @return bool
1129      */
1130     public function is_aggregate_item() {
1131         return ($this->is_category_item() || $this->is_course_item());
1132     }
1134     /**
1135      * Returns the grade item associated with the course
1136      *
1137      * @param int $courseid
1138      * @return grade_item Course level grade item object
1139      */
1140     public static function fetch_course_item($courseid) {
1141         if ($course_item = grade_item::fetch(array('courseid'=>$courseid, 'itemtype'=>'course'))) {
1142             return $course_item;
1143         }
1145         // first get category - it creates the associated grade item
1146         $course_category = grade_category::fetch_course_category($courseid);
1147         return $course_category->get_grade_item();
1148     }
1150     /**
1151      * Is grading object editable?
1152      *
1153      * @return bool
1154      */
1155     public function is_editable() {
1156         return true;
1157     }
1159     /**
1160      * Checks if grade calculated. Returns this object's calculation.
1161      *
1162      * @return bool true if grade item calculated.
1163      */
1164     public function is_calculated() {
1165         if (empty($this->calculation)) {
1166             return false;
1167         }
1169         /*
1170          * The main reason why we use the ##gixxx## instead of [[idnumber]] is speed of depends_on(),
1171          * we would have to fetch all course grade items to find out the ids.
1172          * Also if user changes the idnumber the formula does not need to be updated.
1173          */
1175         // first detect if we need to change calculation formula from [[idnumber]] to ##giXXX## (after backup, etc.)
1176         if (!$this->calculation_normalized and strpos($this->calculation, '[[') !== false) {
1177             $this->set_calculation($this->calculation);
1178         }
1180         return !empty($this->calculation);
1181     }
1183     /**
1184      * Returns calculation string if grade calculated.
1185      *
1186      * @return string Returns the grade item's calculation if calculation is used, null if not
1187      */
1188     public function get_calculation() {
1189         if ($this->is_calculated()) {
1190             return grade_item::denormalize_formula($this->calculation, $this->courseid);
1192         } else {
1193             return NULL;
1194         }
1195     }
1197     /**
1198      * Sets this item's calculation (creates it) if not yet set, or
1199      * updates it if already set (in the DB). If no calculation is given,
1200      * the calculation is removed.
1201      *
1202      * @param string $formula string representation of formula used for calculation
1203      * @return bool success
1204      */
1205     public function set_calculation($formula) {
1206         $this->calculation = grade_item::normalize_formula($formula, $this->courseid);
1207         $this->calculation_normalized = true;
1208         return $this->update();
1209     }
1211     /**
1212      * Denormalizes the calculation formula to [idnumber] form
1213      *
1214      * @param string $formula A string representation of the formula
1215      * @param int $courseid The course ID
1216      * @return string The denormalized formula as a string
1217      */
1218     public static function denormalize_formula($formula, $courseid) {
1219         if (empty($formula)) {
1220             return '';
1221         }
1223         // denormalize formula - convert ##giXX## to [[idnumber]]
1224         if (preg_match_all('/##gi(\d+)##/', $formula, $matches)) {
1225             foreach ($matches[1] as $id) {
1226                 if ($grade_item = grade_item::fetch(array('id'=>$id, 'courseid'=>$courseid))) {
1227                     if (!empty($grade_item->idnumber)) {
1228                         $formula = str_replace('##gi'.$grade_item->id.'##', '[['.$grade_item->idnumber.']]', $formula);
1229                     }
1230                 }
1231             }
1232         }
1234         return $formula;
1236     }
1238     /**
1239      * Normalizes the calculation formula to [#giXX#] form
1240      *
1241      * @param string $formula The formula
1242      * @param int $courseid The course ID
1243      * @return string The normalized formula as a string
1244      */
1245     public static function normalize_formula($formula, $courseid) {
1246         $formula = trim($formula);
1248         if (empty($formula)) {
1249             return NULL;
1251         }
1253         // normalize formula - we want grade item ids ##giXXX## instead of [[idnumber]]
1254         if ($grade_items = grade_item::fetch_all(array('courseid'=>$courseid))) {
1255             foreach ($grade_items as $grade_item) {
1256                 $formula = str_replace('[['.$grade_item->idnumber.']]', '##gi'.$grade_item->id.'##', $formula);
1257             }
1258         }
1260         return $formula;
1261     }
1263     /**
1264      * Returns the final values for this grade item (as imported by module or other source).
1265      *
1266      * @param int $userid Optional: to retrieve a single user's final grade
1267      * @return array|grade_grade An array of all grade_grade instances for this grade_item, or a single grade_grade instance.
1268      */
1269     public function get_final($userid=NULL) {
1270         global $DB;
1271         if ($userid) {
1272             if ($user = $DB->get_record('grade_grades', array('itemid' => $this->id, 'userid' => $userid))) {
1273                 return $user;
1274             }
1276         } else {
1277             if ($grades = $DB->get_records('grade_grades', array('itemid' => $this->id))) {
1278                 //TODO: speed up with better SQL (MDL-31380)
1279                 $result = array();
1280                 foreach ($grades as $grade) {
1281                     $result[$grade->userid] = $grade;
1282                 }
1283                 return $result;
1284             } else {
1285                 return array();
1286             }
1287         }
1288     }
1290     /**
1291      * Get (or create if not exist yet) grade for this user
1292      *
1293      * @param int $userid The user ID
1294      * @param bool $create If true and the user has no grade for this grade item a new grade_grade instance will be inserted
1295      * @return grade_grade The grade_grade instance for the user for this grade item
1296      */
1297     public function get_grade($userid, $create=true) {
1298         if (empty($this->id)) {
1299             debugging('Can not use before insert');
1300             return false;
1301         }
1303         $grade = new grade_grade(array('userid'=>$userid, 'itemid'=>$this->id));
1304         if (empty($grade->id) and $create) {
1305             $grade->insert();
1306         }
1308         return $grade;
1309     }
1311     /**
1312      * Returns the sortorder of this grade_item. This method is also available in
1313      * grade_category, for cases where the object type is not know.
1314      *
1315      * @return int Sort order
1316      */
1317     public function get_sortorder() {
1318         return $this->sortorder;
1319     }
1321     /**
1322      * Returns the idnumber of this grade_item. This method is also available in
1323      * grade_category, for cases where the object type is not know.
1324      *
1325      * @return string The grade item idnumber
1326      */
1327     public function get_idnumber() {
1328         return $this->idnumber;
1329     }
1331     /**
1332      * Returns this grade_item. This method is also available in
1333      * grade_category, for cases where the object type is not know.
1334      *
1335      * @return grade_item
1336      */
1337     public function get_grade_item() {
1338         return $this;
1339     }
1341     /**
1342      * Sets the sortorder of this grade_item. This method is also available in
1343      * grade_category, for cases where the object type is not know.
1344      *
1345      * @param int $sortorder
1346      */
1347     public function set_sortorder($sortorder) {
1348         if ($this->sortorder == $sortorder) {
1349             return;
1350         }
1351         $this->sortorder = $sortorder;
1352         $this->update();
1353     }
1355     /**
1356      * Update this grade item's sortorder so that it will appear after $sortorder
1357      *
1358      * @param int $sortorder The sort order to place this grade item after
1359      */
1360     public function move_after_sortorder($sortorder) {
1361         global $CFG, $DB;
1363         //make some room first
1364         $params = array($sortorder, $this->courseid);
1365         $sql = "UPDATE {grade_items}
1366                    SET sortorder = sortorder + 1
1367                  WHERE sortorder > ? AND courseid = ?";
1368         $DB->execute($sql, $params);
1370         $this->set_sortorder($sortorder + 1);
1371     }
1373     /**
1374      * Detect duplicate grade item's sortorder and re-sort them.
1375      * Note: Duplicate sortorder will be introduced while duplicating activities or
1376      * merging two courses.
1377      *
1378      * @param int $courseid id of the course for which grade_items sortorder need to be fixed.
1379      */
1380     public static function fix_duplicate_sortorder($courseid) {
1381         global $DB;
1383         $transaction = $DB->start_delegated_transaction();
1385         $sql = "SELECT DISTINCT g1.id, g1.courseid, g1.sortorder
1386                     FROM {grade_items} g1
1387                     JOIN {grade_items} g2 ON g1.courseid = g2.courseid
1388                 WHERE g1.sortorder = g2.sortorder AND g1.id != g2.id AND g1.courseid = :courseid
1389                 ORDER BY g1.sortorder DESC, g1.id DESC";
1391         // Get all duplicates in course highest sort order, and higest id first so that we can make space at the
1392         // bottom higher end of the sort orders and work down by id.
1393         $rs = $DB->get_recordset_sql($sql, array('courseid' => $courseid));
1395         foreach($rs as $duplicate) {
1396             $DB->execute("UPDATE {grade_items}
1397                             SET sortorder = sortorder + 1
1398                           WHERE courseid = :courseid AND
1399                           (sortorder > :sortorder OR (sortorder = :sortorder2 AND id > :id))",
1400                 array('courseid' => $duplicate->courseid,
1401                     'sortorder' => $duplicate->sortorder,
1402                     'sortorder2' => $duplicate->sortorder,
1403                     'id' => $duplicate->id));
1404         }
1405         $rs->close();
1406         $transaction->allow_commit();
1407     }
1409     /**
1410      * Returns the most descriptive field for this object.
1411      *
1412      * Determines what type of grade item it is then returns the appropriate string
1413      *
1414      * @param bool $fulltotal If the item is a category total, returns $categoryname."total" instead of "Category total" or "Course total"
1415      * @return string name
1416      */
1417     public function get_name($fulltotal=false) {
1418         global $CFG;
1419         require_once($CFG->dirroot . '/course/lib.php');
1420         if (strval($this->itemname) !== '') {
1421             // MDL-10557
1423             // Make it obvious to users if the course module to which this grade item relates, is currently being removed.
1424             $deletionpending = course_module_instance_pending_deletion($this->courseid, $this->itemmodule, $this->iteminstance);
1425             $deletionnotice = get_string('gradesmoduledeletionprefix', 'grades');
1427             $options = ['context' => context_course::instance($this->courseid)];
1428             return $deletionpending ?
1429                 format_string($deletionnotice . ' ' . $this->itemname, true, $options) :
1430                 format_string($this->itemname, true, $options);
1432         } else if ($this->is_course_item()) {
1433             return get_string('coursetotal', 'grades');
1435         } else if ($this->is_category_item()) {
1436             if ($fulltotal) {
1437                 $category = $this->load_parent_category();
1438                 $a = new stdClass();
1439                 $a->category = $category->get_name();
1440                 return get_string('categorytotalfull', 'grades', $a);
1441             } else {
1442             return get_string('categorytotal', 'grades');
1443             }
1445         } else {
1446             return get_string('grade');
1447         }
1448     }
1450     /**
1451      * A grade item can return a more detailed description which will be added to the header of the column/row in some reports.
1452      *
1453      * @return string description
1454      */
1455     public function get_description() {
1456         if ($this->is_course_item() || $this->is_category_item()) {
1457             $categoryitem = $this->load_item_category();
1458             return $categoryitem->get_description();
1459         }
1460         return '';
1461     }
1463     /**
1464      * Sets this item's categoryid. A generic method shared by objects that have a parent id of some kind.
1465      *
1466      * @param int $parentid The ID of the new parent
1467      * @param bool $updateaggregationfields Whether or not to convert the aggregation fields when switching between category.
1468      *                          Set this to false when the aggregation fields have been updated in prevision of the new
1469      *                          category, typically when the item is freshly created.
1470      * @return bool True if success
1471      */
1472     public function set_parent($parentid, $updateaggregationfields = true) {
1473         if ($this->is_course_item() or $this->is_category_item()) {
1474             print_error('cannotsetparentforcatoritem');
1475         }
1477         if ($this->categoryid == $parentid) {
1478             return true;
1479         }
1481         // find parent and check course id
1482         if (!$parent_category = grade_category::fetch(array('id'=>$parentid, 'courseid'=>$this->courseid))) {
1483             return false;
1484         }
1486         $currentparent = $this->load_parent_category();
1488         if ($updateaggregationfields) {
1489             $this->set_aggregation_fields_for_aggregation($currentparent->aggregation, $parent_category->aggregation);
1490         }
1492         $this->force_regrading();
1494         // set new parent
1495         $this->categoryid = $parent_category->id;
1496         $this->parent_category =& $parent_category;
1498         return $this->update();
1499     }
1501     /**
1502      * Update the aggregation fields when the aggregation changed.
1503      *
1504      * This method should always be called when the aggregation has changed, but also when
1505      * the item was moved to another category, even it if uses the same aggregation method.
1506      *
1507      * Some values such as the weight only make sense within a category, once moved the
1508      * values should be reset to let the user adapt them accordingly.
1509      *
1510      * Note that this method does not save the grade item.
1511      * {@link grade_item::update()} has to be called manually after using this method.
1512      *
1513      * @param  int $from Aggregation method constant value.
1514      * @param  int $to   Aggregation method constant value.
1515      * @return boolean   True when at least one field was changed, false otherwise
1516      */
1517     public function set_aggregation_fields_for_aggregation($from, $to) {
1518         $defaults = grade_category::get_default_aggregation_coefficient_values($to);
1520         $origaggregationcoef = $this->aggregationcoef;
1521         $origaggregationcoef2 = $this->aggregationcoef2;
1522         $origweighoverride = $this->weightoverride;
1524         if ($from == GRADE_AGGREGATE_SUM && $to == GRADE_AGGREGATE_SUM && $this->weightoverride) {
1525             // Do nothing. We are switching from SUM to SUM and the weight is overriden,
1526             // a teacher would not expect any change in this situation.
1528         } else if ($from == GRADE_AGGREGATE_WEIGHTED_MEAN && $to == GRADE_AGGREGATE_WEIGHTED_MEAN) {
1529             // Do nothing. The weights can be kept in this case.
1531         } else if (in_array($from, array(GRADE_AGGREGATE_SUM,  GRADE_AGGREGATE_EXTRACREDIT_MEAN, GRADE_AGGREGATE_WEIGHTED_MEAN2))
1532                 && in_array($to, array(GRADE_AGGREGATE_SUM,  GRADE_AGGREGATE_EXTRACREDIT_MEAN, GRADE_AGGREGATE_WEIGHTED_MEAN2))) {
1534             // Reset all but the the extra credit field.
1535             $this->aggregationcoef2 = $defaults['aggregationcoef2'];
1536             $this->weightoverride = $defaults['weightoverride'];
1538             if ($to != GRADE_AGGREGATE_EXTRACREDIT_MEAN) {
1539                 // Normalise extra credit, except for 'Mean with extra credit' which supports higher values than 1.
1540                 $this->aggregationcoef = min(1, $this->aggregationcoef);
1541             }
1542         } else {
1543             // Reset all.
1544             $this->aggregationcoef = $defaults['aggregationcoef'];
1545             $this->aggregationcoef2 = $defaults['aggregationcoef2'];
1546             $this->weightoverride = $defaults['weightoverride'];
1547         }
1549         $acoefdiff       = grade_floats_different($origaggregationcoef, $this->aggregationcoef);
1550         $acoefdiff2      = grade_floats_different($origaggregationcoef2, $this->aggregationcoef2);
1551         $weightoverride  = grade_floats_different($origweighoverride, $this->weightoverride);
1553         return $acoefdiff || $acoefdiff2 || $weightoverride;
1554     }
1556     /**
1557      * Makes sure value is a valid grade value.
1558      *
1559      * @param float $gradevalue
1560      * @return mixed float or int fixed grade value
1561      */
1562     public function bounded_grade($gradevalue) {
1563         global $CFG;
1565         if (is_null($gradevalue)) {
1566             return null;
1567         }
1569         if ($this->gradetype == GRADE_TYPE_SCALE) {
1570             // no >100% grades hack for scale grades!
1571             // 1.5 is rounded to 2 ;-)
1572             return (int)bounded_number($this->grademin, round($gradevalue+0.00001), $this->grademax);
1573         }
1575         $grademax = $this->grademax;
1577         // NOTE: if you change this value you must manually reset the needsupdate flag in all grade items
1578         $maxcoef = isset($CFG->gradeoverhundredprocentmax) ? $CFG->gradeoverhundredprocentmax : 10; // 1000% max by default
1580         if (!empty($CFG->unlimitedgrades)) {
1581             // NOTE: if you change this value you must manually reset the needsupdate flag in all grade items
1582             $grademax = $grademax * $maxcoef;
1583         } else if ($this->is_category_item() or $this->is_course_item()) {
1584             $category = $this->load_item_category();
1585             if ($category->aggregation >= 100) {
1586                 // grade >100% hack
1587                 $grademax = $grademax * $maxcoef;
1588             }
1589         }
1591         return (float)bounded_number($this->grademin, $gradevalue, $grademax);
1592     }
1594     /**
1595      * Finds out on which other items does this depend directly when doing calculation or category aggregation
1596      *
1597      * @param bool $reset_cache
1598      * @return array of grade_item IDs this one depends on
1599      */
1600     public function depends_on($reset_cache=false) {
1601         global $CFG, $DB;
1603         if ($reset_cache) {
1604             $this->dependson_cache = null;
1605         } else if (isset($this->dependson_cache)) {
1606             return $this->dependson_cache;
1607         }
1609         if ($this->is_locked()) {
1610             // locked items do not need to be regraded
1611             $this->dependson_cache = array();
1612             return $this->dependson_cache;
1613         }
1615         if ($this->is_calculated()) {
1616             if (preg_match_all('/##gi(\d+)##/', $this->calculation, $matches)) {
1617                 $this->dependson_cache = array_unique($matches[1]); // remove duplicates
1618                 return $this->dependson_cache;
1619             } else {
1620                 $this->dependson_cache = array();
1621                 return $this->dependson_cache;
1622             }
1624         } else if ($grade_category = $this->load_item_category()) {
1625             $params = array();
1627             //only items with numeric or scale values can be aggregated
1628             if ($this->gradetype != GRADE_TYPE_VALUE and $this->gradetype != GRADE_TYPE_SCALE) {
1629                 $this->dependson_cache = array();
1630                 return $this->dependson_cache;
1631             }
1633             $grade_category->apply_forced_settings();
1635             if (empty($CFG->enableoutcomes) or $grade_category->aggregateoutcomes) {
1636                 $outcomes_sql = "";
1637             } else {
1638                 $outcomes_sql = "AND gi.outcomeid IS NULL";
1639             }
1641             if (empty($CFG->grade_includescalesinaggregation)) {
1642                 $gtypes = "gi.gradetype = ?";
1643                 $params[] = GRADE_TYPE_VALUE;
1644             } else {
1645                 $gtypes = "(gi.gradetype = ? OR gi.gradetype = ?)";
1646                 $params[] = GRADE_TYPE_VALUE;
1647                 $params[] = GRADE_TYPE_SCALE;
1648             }
1650             $params[] = $grade_category->id;
1651             $params[] = $this->courseid;
1652             $params[] = $grade_category->id;
1653             $params[] = $this->courseid;
1654             if (empty($CFG->grade_includescalesinaggregation)) {
1655                 $params[] = GRADE_TYPE_VALUE;
1656             } else {
1657                 $params[] = GRADE_TYPE_VALUE;
1658                 $params[] = GRADE_TYPE_SCALE;
1659             }
1660             $sql = "SELECT gi.id
1661                       FROM {grade_items} gi
1662                      WHERE $gtypes
1663                            AND gi.categoryid = ?
1664                            AND gi.courseid = ?
1665                            $outcomes_sql
1666                     UNION
1668                     SELECT gi.id
1669                       FROM {grade_items} gi, {grade_categories} gc
1670                      WHERE (gi.itemtype = 'category' OR gi.itemtype = 'course') AND gi.iteminstance=gc.id
1671                            AND gc.parent = ?
1672                            AND gi.courseid = ?
1673                            AND $gtypes
1674                            $outcomes_sql";
1676             if ($children = $DB->get_records_sql($sql, $params)) {
1677                 $this->dependson_cache = array_keys($children);
1678                 return $this->dependson_cache;
1679             } else {
1680                 $this->dependson_cache = array();
1681                 return $this->dependson_cache;
1682             }
1684         } else {
1685             $this->dependson_cache = array();
1686             return $this->dependson_cache;
1687         }
1688     }
1690     /**
1691      * Refetch grades from modules, plugins.
1692      *
1693      * @param int $userid optional, limit the refetch to a single user
1694      * @return bool Returns true on success or if there is nothing to do
1695      */
1696     public function refresh_grades($userid=0) {
1697         global $DB;
1698         if ($this->itemtype == 'mod') {
1699             if ($this->is_outcome_item()) {
1700                 //nothing to do
1701                 return true;
1702             }
1704             if (!$activity = $DB->get_record($this->itemmodule, array('id' => $this->iteminstance))) {
1705                 debugging("Can not find $this->itemmodule activity with id $this->iteminstance");
1706                 return false;
1707             }
1709             if (!$cm = get_coursemodule_from_instance($this->itemmodule, $activity->id, $this->courseid)) {
1710                 debugging('Can not find course module');
1711                 return false;
1712             }
1714             $activity->modname    = $this->itemmodule;
1715             $activity->cmidnumber = $cm->idnumber;
1717             return grade_update_mod_grades($activity, $userid);
1718         }
1720         return true;
1721     }
1723     /**
1724      * Updates final grade value for given user, this is a only way to update final
1725      * grades from gradebook and import because it logs the change in history table
1726      * and deals with overridden flag. This flag is set to prevent later overriding
1727      * from raw grades submitted from modules.
1728      *
1729      * @param int $userid The graded user
1730      * @param float|false $finalgrade The float value of final grade, false means do not change
1731      * @param string $source The modification source
1732      * @param string $feedback Optional teacher feedback
1733      * @param int $feedbackformat A format like FORMAT_PLAIN or FORMAT_HTML
1734      * @param int $usermodified The ID of the user making the modification
1735      * @return bool success
1736      */
1737     public function update_final_grade($userid, $finalgrade=false, $source=NULL, $feedback=false, $feedbackformat=FORMAT_MOODLE, $usermodified=null) {
1738         global $USER, $CFG;
1740         $result = true;
1742         // no grading used or locked
1743         if ($this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) {
1744             return false;
1745         }
1747         $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid));
1748         $grade->grade_item =& $this; // prevent db fetching of this grade_item
1750         if (empty($usermodified)) {
1751             $grade->usermodified = $USER->id;
1752         } else {
1753             $grade->usermodified = $usermodified;
1754         }
1756         if ($grade->is_locked()) {
1757             // do not update locked grades at all
1758             return false;
1759         }
1761         $locktime = $grade->get_locktime();
1762         if ($locktime and $locktime < time()) {
1763             // do not update grades that should be already locked, force regrade instead
1764             $this->force_regrading();
1765             return false;
1766         }
1768         $oldgrade = new stdClass();
1769         $oldgrade->finalgrade     = $grade->finalgrade;
1770         $oldgrade->overridden     = $grade->overridden;
1771         $oldgrade->feedback       = $grade->feedback;
1772         $oldgrade->feedbackformat = $grade->feedbackformat;
1773         $oldgrade->rawgrademin    = $grade->rawgrademin;
1774         $oldgrade->rawgrademax    = $grade->rawgrademax;
1776         // MDL-31713 rawgramemin and max must be up to date so conditional access %'s works properly.
1777         $grade->rawgrademin = $this->grademin;
1778         $grade->rawgrademax = $this->grademax;
1779         $grade->rawscaleid  = $this->scaleid;
1781         // changed grade?
1782         if ($finalgrade !== false) {
1783             if ($this->is_overridable_item()) {
1784                 $grade->overridden = time();
1785             }
1787             $grade->finalgrade = $this->bounded_grade($finalgrade);
1788         }
1790         // do we have comment from teacher?
1791         if ($feedback !== false) {
1792             if ($this->is_overridable_item_feedback()) {
1793                 // external items (modules, plugins) may have own feedback
1794                 $grade->overridden = time();
1795             }
1797             $grade->feedback       = $feedback;
1798             $grade->feedbackformat = $feedbackformat;
1799         }
1801         $gradechanged = false;
1802         if (empty($grade->id)) {
1803             $grade->timecreated  = null;   // hack alert - date submitted - no submission yet
1804             $grade->timemodified = time(); // hack alert - date graded
1805             $result = (bool)$grade->insert($source);
1807             // If the grade insert was successful and the final grade was not null then trigger a user_graded event.
1808             if ($result && !is_null($grade->finalgrade)) {
1809                 \core\event\user_graded::create_from_grade($grade)->trigger();
1810             }
1811             $gradechanged = true;
1812         } else {
1813             // Existing grade_grades.
1815             if (grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)
1816                     or grade_floats_different($grade->rawgrademin, $oldgrade->rawgrademin)
1817                     or grade_floats_different($grade->rawgrademax, $oldgrade->rawgrademax)
1818                     or ($oldgrade->overridden == 0 and $grade->overridden > 0)) {
1819                 $gradechanged = true;
1820             }
1822             if ($grade->feedback === $oldgrade->feedback and $grade->feedbackformat == $oldgrade->feedbackformat and
1823                     $gradechanged === false) {
1824                 // No grade nor feedback changed.
1825                 return $result;
1826             }
1828             $grade->timemodified = time(); // hack alert - date graded
1829             $result = $grade->update($source);
1831             // If the grade update was successful and the actual grade has changed then trigger a user_graded event.
1832             if ($result && grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)) {
1833                 \core\event\user_graded::create_from_grade($grade)->trigger();
1834             }
1835         }
1837         if (!$result) {
1838             // Something went wrong - better force final grade recalculation.
1839             $this->force_regrading();
1840             return $result;
1841         }
1843         // If we are not updating grades we don't need to recalculate the whole course.
1844         if (!$gradechanged) {
1845             return $result;
1846         }
1848         if ($this->is_course_item() and !$this->needsupdate) {
1849             if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
1850                 $this->force_regrading();
1851             }
1853         } else if (!$this->needsupdate) {
1855             $course_item = grade_item::fetch_course_item($this->courseid);
1856             if (!$course_item->needsupdate) {
1857                 if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
1858                     $this->force_regrading();
1859                 }
1860             } else {
1861                 $this->force_regrading();
1862             }
1863         }
1865         return $result;
1866     }
1869     /**
1870      * Updates raw grade value for given user, this is a only way to update raw
1871      * grades from external source (modules, etc.),
1872      * because it logs the change in history table and deals with final grade recalculation.
1873      *
1874      * @param int $userid the graded user
1875      * @param mixed $rawgrade float value of raw grade - false means do not change
1876      * @param string $source modification source
1877      * @param string $feedback optional teacher feedback
1878      * @param int $feedbackformat A format like FORMAT_PLAIN or FORMAT_HTML
1879      * @param int $usermodified the ID of the user who did the grading
1880      * @param int $dategraded A timestamp of when the student's work was graded
1881      * @param int $datesubmitted A timestamp of when the student's work was submitted
1882      * @param grade_grade $grade A grade object, useful for bulk upgrades
1883      * @param array $feedbackfiles An array identifying the location of files we want to copy to the gradebook feedback area.
1884      *        Example -
1885      *        [
1886      *            'contextid' => 1,
1887      *            'component' => 'mod_xyz',
1888      *            'filearea' => 'mod_xyz_feedback',
1889      *            'itemid' => 2
1890      *        ];
1891      * @return bool success
1892      */
1893     public function update_raw_grade($userid, $rawgrade = false, $source = null, $feedback = false,
1894             $feedbackformat = FORMAT_MOODLE, $usermodified = null, $dategraded = null, $datesubmitted=null,
1895             $grade = null, array $feedbackfiles = []) {
1896         global $USER;
1898         $result = true;
1900         // calculated grades can not be updated; course and category can not be updated  because they are aggregated
1901         if (!$this->is_raw_used() or $this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) {
1902             return false;
1903         }
1905         if (is_null($grade)) {
1906             //fetch from db
1907             $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid));
1908         }
1909         $grade->grade_item =& $this; // prevent db fetching of this grade_item
1911         if (empty($usermodified)) {
1912             $grade->usermodified = $USER->id;
1913         } else {
1914             $grade->usermodified = $usermodified;
1915         }
1917         if ($grade->is_locked()) {
1918             // do not update locked grades at all
1919             return false;
1920         }
1922         $locktime = $grade->get_locktime();
1923         if ($locktime and $locktime < time()) {
1924             // do not update grades that should be already locked and force regrade
1925             $this->force_regrading();
1926             return false;
1927         }
1929         $oldgrade = new stdClass();
1930         $oldgrade->finalgrade     = $grade->finalgrade;
1931         $oldgrade->rawgrade       = $grade->rawgrade;
1932         $oldgrade->rawgrademin    = $grade->rawgrademin;
1933         $oldgrade->rawgrademax    = $grade->rawgrademax;
1934         $oldgrade->rawscaleid     = $grade->rawscaleid;
1935         $oldgrade->feedback       = $grade->feedback;
1936         $oldgrade->feedbackformat = $grade->feedbackformat;
1938         // use new min and max
1939         $grade->rawgrade    = $grade->rawgrade;
1940         $grade->rawgrademin = $this->grademin;
1941         $grade->rawgrademax = $this->grademax;
1942         $grade->rawscaleid  = $this->scaleid;
1944         // change raw grade?
1945         if ($rawgrade !== false) {
1946             $grade->rawgrade = $rawgrade;
1947         }
1949         // empty feedback means no feedback at all
1950         if ($feedback === '') {
1951             $feedback = null;
1952         }
1954         // do we have comment from teacher?
1955         if ($feedback !== false and !$grade->is_overridden()) {
1956             $grade->feedback       = $feedback;
1957             $grade->feedbackformat = $feedbackformat;
1958             $grade->feedbackfiles  = $feedbackfiles;
1959         }
1961         // update final grade if possible
1962         if (!$grade->is_locked() and !$grade->is_overridden()) {
1963             $grade->finalgrade = $this->adjust_raw_grade($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
1964         }
1966         // TODO: hack alert - create new fields for these in 2.0
1967         $oldgrade->timecreated  = $grade->timecreated;
1968         $oldgrade->timemodified = $grade->timemodified;
1970         $grade->timecreated = $datesubmitted;
1972         if ($grade->is_overridden()) {
1973             // keep original graded date - update_final_grade() sets this for overridden grades
1975         } else if (is_null($grade->rawgrade) and is_null($grade->feedback)) {
1976             // no grade and feedback means no grading yet
1977             $grade->timemodified = null;
1979         } else if (!empty($dategraded)) {
1980             // fine - module sends info when graded (yay!)
1981             $grade->timemodified = $dategraded;
1983         } else if (grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)
1984                    or $grade->feedback !== $oldgrade->feedback) {
1985             // guess - if either grade or feedback changed set new graded date
1986             $grade->timemodified = time();
1988         } else {
1989             //keep original graded date
1990         }
1991         // end of hack alert
1993         $gradechanged = false;
1994         if (empty($grade->id)) {
1995             $result = (bool)$grade->insert($source);
1997             // If the grade insert was successful and the final grade was not null then trigger a user_graded event.
1998             if ($result && !is_null($grade->finalgrade)) {
1999                 \core\event\user_graded::create_from_grade($grade)->trigger();
2000             }
2001             $gradechanged = true;
2002         } else {
2003             // Existing grade_grades.
2005             if (grade_floats_different($grade->finalgrade,  $oldgrade->finalgrade)
2006                     or grade_floats_different($grade->rawgrade,    $oldgrade->rawgrade)
2007                     or grade_floats_different($grade->rawgrademin, $oldgrade->rawgrademin)
2008                     or grade_floats_different($grade->rawgrademax, $oldgrade->rawgrademax)
2009                     or $grade->rawscaleid != $oldgrade->rawscaleid) {
2010                 $gradechanged = true;
2011             }
2013             // The timecreated and timemodified checking is part of the hack above.
2014             if ($gradechanged === false and
2015                     $grade->feedback === $oldgrade->feedback and
2016                     $grade->feedbackformat == $oldgrade->feedbackformat and
2017                     $grade->timecreated == $oldgrade->timecreated and
2018                     $grade->timemodified == $oldgrade->timemodified) {
2019                 // No changes.
2020                 return $result;
2021             }
2022             $result = $grade->update($source);
2024             // If the grade update was successful and the actual grade has changed then trigger a user_graded event.
2025             if ($result && grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)) {
2026                 \core\event\user_graded::create_from_grade($grade)->trigger();
2027             }
2028         }
2030         if (!$result) {
2031             // Something went wrong - better force final grade recalculation.
2032             $this->force_regrading();
2033             return $result;
2034         }
2036         // If we are not updating grades we don't need to recalculate the whole course.
2037         if (!$gradechanged) {
2038             return $result;
2039         }
2041         if (!$this->needsupdate) {
2042             $course_item = grade_item::fetch_course_item($this->courseid);
2043             if (!$course_item->needsupdate) {
2044                 if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
2045                     $this->force_regrading();
2046                 }
2047             }
2048         }
2050         return $result;
2051     }
2053     /**
2054      * Calculates final grade values using the formula in the calculation property.
2055      * The parameters are taken from final grades of grade items in current course only.
2056      *
2057      * @param int $userid Supply a user ID to limit the calculations to the grades of a single user
2058      * @return bool false if error
2059      */
2060     public function compute($userid=null) {
2061         global $CFG, $DB;
2063         if (!$this->is_calculated()) {
2064             return false;
2065         }
2067         require_once($CFG->libdir.'/mathslib.php');
2069         if ($this->is_locked()) {
2070             return true; // no need to recalculate locked items
2071         }
2073         // Precreate grades - we need them to exist
2074         if ($userid) {
2075             $missing = array();
2076             if (!$DB->record_exists('grade_grades', array('itemid'=>$this->id, 'userid'=>$userid))) {
2077                 $m = new stdClass();
2078                 $m->userid = $userid;
2079                 $missing[] = $m;
2080             }
2081         } else {
2082             // Find any users who have grades for some but not all grade items in this course
2083             $params = array('gicourseid' => $this->courseid, 'ggitemid' => $this->id);
2084             $sql = "SELECT gg.userid
2085                       FROM {grade_grades} gg
2086                            JOIN {grade_items} gi
2087                            ON (gi.id = gg.itemid AND gi.courseid = :gicourseid)
2088                      GROUP BY gg.userid
2089                      HAVING SUM(CASE WHEN gg.itemid = :ggitemid THEN 1 ELSE 0 END) = 0";
2090             $missing = $DB->get_records_sql($sql, $params);
2091         }
2093         if ($missing) {
2094             foreach ($missing as $m) {
2095                 $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$m->userid), false);
2096                 $grade->grade_item =& $this;
2097                 $grade->insert('system');
2098             }
2099         }
2101         // get used items
2102         $useditems = $this->depends_on();
2104         // prepare formula and init maths library
2105         $formula = preg_replace('/##(gi\d+)##/', '\1', $this->calculation);
2106         if (strpos($formula, '[[') !== false) {
2107             // missing item
2108             return false;
2109         }
2110         $this->formula = new calc_formula($formula);
2112         // where to look for final grades?
2113         // this itemid is added so that we use only one query for source and final grades
2114         $gis = array_merge($useditems, array($this->id));
2115         list($usql, $params) = $DB->get_in_or_equal($gis);
2117         if ($userid) {
2118             $usersql = "AND g.userid=?";
2119             $params[] = $userid;
2120         } else {
2121             $usersql = "";
2122         }
2124         $grade_inst = new grade_grade();
2125         $fields = 'g.'.implode(',g.', $grade_inst->required_fields);
2127         $params[] = $this->courseid;
2128         $sql = "SELECT $fields
2129                   FROM {grade_grades} g, {grade_items} gi
2130                  WHERE gi.id = g.itemid AND gi.id $usql $usersql AND gi.courseid=?
2131                  ORDER BY g.userid";
2133         $return = true;
2135         // group the grades by userid and use formula on the group
2136         $rs = $DB->get_recordset_sql($sql, $params);
2137         if ($rs->valid()) {
2138             $prevuser = 0;
2139             $grade_records   = array();
2140             $oldgrade    = null;
2141             foreach ($rs as $used) {
2142                 if ($used->userid != $prevuser) {
2143                     if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {
2144                         $return = false;
2145                     }
2146                     $prevuser = $used->userid;
2147                     $grade_records   = array();
2148                     $oldgrade    = null;
2149                 }
2150                 if ($used->itemid == $this->id) {
2151                     $oldgrade = $used;
2152                 }
2153                 $grade_records['gi'.$used->itemid] = $used->finalgrade;
2154             }
2155             if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {
2156                 $return = false;
2157             }
2158         }
2159         $rs->close();
2161         return $return;
2162     }
2164     /**
2165      * Internal function that does the final grade calculation
2166      *
2167      * @param int $userid The user ID
2168      * @param array $params An array of grade items of the form {'gi'.$itemid]} => $finalgrade
2169      * @param array $useditems An array of grade item IDs that this grade item depends on plus its own ID
2170      * @param grade_grade $oldgrade A grade_grade instance containing the old values from the database
2171      * @return bool False if an error occurred
2172      */
2173     public function use_formula($userid, $params, $useditems, $oldgrade) {
2174         if (empty($userid)) {
2175             return true;
2176         }
2178         // add missing final grade values
2179         // not graded (null) is counted as 0 - the spreadsheet way
2180         $allinputsnull = true;
2181         foreach($useditems as $gi) {
2182             if (!array_key_exists('gi'.$gi, $params) || is_null($params['gi'.$gi])) {
2183                 $params['gi'.$gi] = 0;
2184             } else {
2185                 $params['gi'.$gi] = (float)$params['gi'.$gi];
2186                 if ($gi != $this->id) {
2187                     $allinputsnull = false;
2188                 }
2189             }
2190         }
2192         // can not use own final grade during calculation
2193         unset($params['gi'.$this->id]);
2195         // Check to see if the gradebook is frozen. This allows grades to not be altered at all until a user verifies that they
2196         // wish to update the grades.
2197         $gradebookcalculationsfreeze = get_config('core', 'gradebook_calculations_freeze_' . $this->courseid);
2199         $rawminandmaxchanged = false;
2200         // insert final grade - will be needed later anyway
2201         if ($oldgrade) {
2202             // Only run through this code if the gradebook isn't frozen.
2203             if ($gradebookcalculationsfreeze && (int)$gradebookcalculationsfreeze <= 20150627) {
2204                 // Do nothing.
2205             } else {
2206                 // The grade_grade for a calculated item should have the raw grade maximum and minimum set to the
2207                 // grade_item grade maximum and minimum respectively.
2208                 if ($oldgrade->rawgrademax != $this->grademax || $oldgrade->rawgrademin != $this->grademin) {
2209                     $rawminandmaxchanged = true;
2210                     $oldgrade->rawgrademax = $this->grademax;
2211                     $oldgrade->rawgrademin = $this->grademin;
2212                 }
2213             }
2214             $oldfinalgrade = $oldgrade->finalgrade;
2215             $grade = new grade_grade($oldgrade, false); // fetching from db is not needed
2216             $grade->grade_item =& $this;
2218         } else {
2219             $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid), false);
2220             $grade->grade_item =& $this;
2221             $rawminandmaxchanged = false;
2222             if ($gradebookcalculationsfreeze && (int)$gradebookcalculationsfreeze <= 20150627) {
2223                 // Do nothing.
2224             } else {
2225                 // The grade_grade for a calculated item should have the raw grade maximum and minimum set to the
2226                 // grade_item grade maximum and minimum respectively.
2227                 $rawminandmaxchanged = true;
2228                 $grade->rawgrademax = $this->grademax;
2229                 $grade->rawgrademin = $this->grademin;
2230             }
2231             $grade->insert('system');
2232             $oldfinalgrade = null;
2233         }
2235         // no need to recalculate locked or overridden grades
2236         if ($grade->is_locked() or $grade->is_overridden()) {
2237             return true;
2238         }
2240         if ($allinputsnull) {
2241             $grade->finalgrade = null;
2242             $result = true;
2244         } else {
2246             // do the calculation
2247             $this->formula->set_params($params);
2248             $result = $this->formula->evaluate();
2250             if ($result === false) {
2251                 $grade->finalgrade = null;
2253             } else {
2254                 // normalize
2255                 $grade->finalgrade = $this->bounded_grade($result);
2256             }
2257         }
2259         // Only run through this code if the gradebook isn't frozen.
2260         if ($gradebookcalculationsfreeze && (int)$gradebookcalculationsfreeze <= 20150627) {
2261             // Update in db if changed.
2262             if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) {
2263                 $grade->timemodified = time();
2264                 $success = $grade->update('compute');
2266                 // If successful trigger a user_graded event.
2267                 if ($success) {
2268                     \core\event\user_graded::create_from_grade($grade)->trigger();
2269                 }
2270             }
2271         } else {
2272             // Update in db if changed.
2273             if (grade_floats_different($grade->finalgrade, $oldfinalgrade) || $rawminandmaxchanged) {
2274                 $grade->timemodified = time();
2275                 $success = $grade->update('compute');
2277                 // If successful trigger a user_graded event.
2278                 if ($success) {
2279                     \core\event\user_graded::create_from_grade($grade)->trigger();
2280                 }
2281             }
2282         }
2284         if ($result !== false) {
2285             //lock grade if needed
2286         }
2288         if ($result === false) {
2289             return false;
2290         } else {
2291             return true;
2292         }
2294     }
2296     /**
2297      * Validate the formula.
2298      *
2299      * @param string $formulastr
2300      * @return bool true if calculation possible, false otherwise
2301      */
2302     public function validate_formula($formulastr) {
2303         global $CFG, $DB;
2304         require_once($CFG->libdir.'/mathslib.php');
2306         $formulastr = grade_item::normalize_formula($formulastr, $this->courseid);
2308         if (empty($formulastr)) {
2309             return true;
2310         }
2312         if (strpos($formulastr, '=') !== 0) {
2313             return get_string('errorcalculationnoequal', 'grades');
2314         }
2316         // get used items
2317         if (preg_match_all('/##gi(\d+)##/', $formulastr, $matches)) {
2318             $useditems = array_unique($matches[1]); // remove duplicates
2319         } else {
2320             $useditems = array();
2321         }
2323         // MDL-11902
2324         // unset the value if formula is trying to reference to itself
2325         // but array keys does not match itemid
2326         if (!empty($this->id)) {
2327             $useditems = array_diff($useditems, array($this->id));
2328             //unset($useditems[$this->id]);
2329         }
2331         // prepare formula and init maths library
2332         $formula = preg_replace('/##(gi\d+)##/', '\1', $formulastr);
2333         $formula = new calc_formula($formula);
2336         if (empty($useditems)) {
2337             $grade_items = array();
2339         } else {
2340             list($usql, $params) = $DB->get_in_or_equal($useditems);
2341             $params[] = $this->courseid;
2342             $sql = "SELECT gi.*
2343                       FROM {grade_items} gi
2344                      WHERE gi.id $usql and gi.courseid=?"; // from the same course only!
2346             if (!$grade_items = $DB->get_records_sql($sql, $params)) {
2347                 $grade_items = array();
2348             }
2349         }
2351         $params = array();
2352         foreach ($useditems as $itemid) {
2353             // make sure all grade items exist in this course
2354             if (!array_key_exists($itemid, $grade_items)) {
2355                 return false;
2356             }
2357             // use max grade when testing formula, this should be ok in 99.9%
2358             // division by 0 is one of possible problems
2359             $params['gi'.$grade_items[$itemid]->id] = $grade_items[$itemid]->grademax;
2360         }
2362         // do the calculation
2363         $formula->set_params($params);
2364         $result = $formula->evaluate();
2366         // false as result indicates some problem
2367         if ($result === false) {
2368             // TODO: add more error hints
2369             return get_string('errorcalculationunknown', 'grades');
2370         } else {
2371             return true;
2372         }
2373     }
2375     /**
2376      * Returns the value of the display type
2377      *
2378      * It can be set at 3 levels: grade_item, course setting and site. The lowest level overrides the higher ones.
2379      *
2380      * @return int Display type
2381      */
2382     public function get_displaytype() {
2383         global $CFG;
2385         if ($this->display == GRADE_DISPLAY_TYPE_DEFAULT) {
2386             return grade_get_setting($this->courseid, 'displaytype', $CFG->grade_displaytype);
2388         } else {
2389             return $this->display;
2390         }
2391     }
2393     /**
2394      * Returns the value of the decimals field
2395      *
2396      * It can be set at 3 levels: grade_item, course setting and site. The lowest level overrides the higher ones.
2397      *
2398      * @return int Decimals (0 - 5)
2399      */
2400     public function get_decimals() {
2401         global $CFG;
2403         if (is_null($this->decimals)) {
2404             return grade_get_setting($this->courseid, 'decimalpoints', $CFG->grade_decimalpoints);
2406         } else {
2407             return $this->decimals;
2408         }
2409     }
2411     /**
2412      * Returns a string representing the range of grademin - grademax for this grade item.
2413      *
2414      * @param int $rangesdisplaytype
2415      * @param int $rangesdecimalpoints
2416      * @return string
2417      */
2418     function get_formatted_range($rangesdisplaytype=null, $rangesdecimalpoints=null) {
2420         global $USER;
2422         // Determine which display type to use for this average
2423         if (isset($USER->gradeediting) && array_key_exists($this->courseid, $USER->gradeediting) && $USER->gradeediting[$this->courseid]) {
2424             $displaytype = GRADE_DISPLAY_TYPE_REAL;
2426         } else if ($rangesdisplaytype == GRADE_REPORT_PREFERENCE_INHERIT) { // no ==0 here, please resave report and user prefs
2427             $displaytype = $this->get_displaytype();
2429         } else {
2430             $displaytype = $rangesdisplaytype;
2431         }
2433         // Override grade_item setting if a display preference (not default) was set for the averages
2434         if ($rangesdecimalpoints == GRADE_REPORT_PREFERENCE_INHERIT) {
2435             $decimalpoints = $this->get_decimals();
2437         } else {
2438             $decimalpoints = $rangesdecimalpoints;
2439         }
2441         if ($displaytype == GRADE_DISPLAY_TYPE_PERCENTAGE) {
2442             $grademin = "0 %";
2443             $grademax = "100 %";
2445         } else {
2446             $grademin = grade_format_gradevalue($this->grademin, $this, true, $displaytype, $decimalpoints);
2447             $grademax = grade_format_gradevalue($this->grademax, $this, true, $displaytype, $decimalpoints);
2448         }
2450         return $grademin.'&ndash;'. $grademax;
2451     }
2453     /**
2454      * Queries parent categories recursively to find the aggregationcoef type that applies to this grade item.
2455      *
2456      * @return string|false Returns the coefficient string of false is no coefficient is being used
2457      */
2458     public function get_coefstring() {
2459         $parent_category = $this->load_parent_category();
2460         if ($this->is_category_item()) {
2461             $parent_category = $parent_category->load_parent_category();
2462         }
2464         if ($parent_category->is_aggregationcoef_used()) {
2465             return $parent_category->get_coefstring();
2466         } else {
2467             return false;
2468         }
2469     }
2471     /**
2472      * Returns whether the grade item can control the visibility of the grades
2473      *
2474      * @return bool
2475      */
2476     public function can_control_visibility() {
2477         if (core_component::get_plugin_directory($this->itemtype, $this->itemmodule)) {
2478             return !plugin_supports($this->itemtype, $this->itemmodule, FEATURE_CONTROLS_GRADE_VISIBILITY, false);
2479         }
2480         return parent::can_control_visibility();
2481     }
2483     /**
2484      * Used to notify the completion system (if necessary) that a user's grade
2485      * has changed, and clear up a possible score cache.
2486      *
2487      * @param bool $deleted True if grade was actually deleted
2488      */
2489     protected function notify_changed($deleted) {
2490         global $CFG;
2492         // Condition code may cache the grades for conditional availability of
2493         // modules or sections. (This code should use a hook for communication
2494         // with plugin, but hooks are not implemented at time of writing.)
2495         if (!empty($CFG->enableavailability) && class_exists('\availability_grade\callbacks')) {
2496             \availability_grade\callbacks::grade_item_changed($this->courseid);
2497         }
2498     }
2500     /**
2501      * Helper function to get the accurate context for this grade column.
2502      *
2503      * @return context
2504      */
2505     public function get_context() {
2506         if ($this->itemtype == 'mod') {
2507             $modinfo = get_fast_modinfo($this->courseid);
2508             // Sometimes the course module cache is out of date and needs to be rebuilt.
2509             if (!isset($modinfo->instances[$this->itemmodule][$this->iteminstance])) {
2510                 rebuild_course_cache($this->courseid, true);
2511                 $modinfo = get_fast_modinfo($this->courseid);
2512             }
2513             // Even with a rebuilt cache the module does not exist. This means the
2514             // database is in an invalid state - we will log an error and return
2515             // the course context but the calling code should be updated.
2516             if (!isset($modinfo->instances[$this->itemmodule][$this->iteminstance])) {
2517                 mtrace(get_string('moduleinstancedoesnotexist', 'error'));
2518                 $context = \context_course::instance($this->courseid);
2519             } else {
2520                 $cm = $modinfo->instances[$this->itemmodule][$this->iteminstance];
2521                 $context = \context_module::instance($cm->id);
2522             }
2523         } else {
2524             $context = \context_course::instance($this->courseid);
2525         }
2526         return $context;
2527     }