MDL-54613 unit tests: Add iteminstance to test grade_item
[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         return parent::update($source);
308     }
310     /**
311      * Compares the values held by this object with those of the matching record in DB, and returns
312      * whether or not these differences are sufficient to justify an update of all parent objects.
313      * This assumes that this object has an id number and a matching record in DB. If not, it will return false.
314      *
315      * @return bool
316      */
317     public function qualifies_for_regrading() {
318         if (empty($this->id)) {
319             return false;
320         }
322         $db_item = new grade_item(array('id' => $this->id));
324         $calculationdiff = $db_item->calculation != $this->calculation;
325         $categorydiff    = $db_item->categoryid  != $this->categoryid;
326         $gradetypediff   = $db_item->gradetype   != $this->gradetype;
327         $scaleiddiff     = $db_item->scaleid     != $this->scaleid;
328         $outcomeiddiff   = $db_item->outcomeid   != $this->outcomeid;
329         $locktimediff    = $db_item->locktime    != $this->locktime;
330         $grademindiff    = grade_floats_different($db_item->grademin,        $this->grademin);
331         $grademaxdiff    = grade_floats_different($db_item->grademax,        $this->grademax);
332         $multfactordiff  = grade_floats_different($db_item->multfactor,      $this->multfactor);
333         $plusfactordiff  = grade_floats_different($db_item->plusfactor,      $this->plusfactor);
334         $acoefdiff       = grade_floats_different($db_item->aggregationcoef, $this->aggregationcoef);
335         $acoefdiff2      = grade_floats_different($db_item->aggregationcoef2, $this->aggregationcoef2);
336         $weightoverride  = grade_floats_different($db_item->weightoverride, $this->weightoverride);
338         $needsupdatediff = !$db_item->needsupdate &&  $this->needsupdate;    // force regrading only if setting the flag first time
339         $lockeddiff      = !empty($db_item->locked) && empty($this->locked); // force regrading only when unlocking
341         return ($calculationdiff || $categorydiff || $gradetypediff || $grademaxdiff || $grademindiff || $scaleiddiff
342              || $outcomeiddiff || $multfactordiff || $plusfactordiff || $needsupdatediff
343              || $lockeddiff || $acoefdiff || $acoefdiff2 || $weightoverride || $locktimediff);
344     }
346     /**
347      * Finds and returns a grade_item instance based on params.
348      *
349      * @static
350      * @param array $params associative arrays varname=>value
351      * @return grade_item|bool Returns a grade_item instance or false if none found
352      */
353     public static function fetch($params) {
354         return grade_object::fetch_helper('grade_items', 'grade_item', $params);
355     }
357     /**
358      * Check to see if there are any existing grades for this grade_item.
359      *
360      * @return boolean - true if there are valid grades for this grade_item.
361      */
362     public function has_grades() {
363         global $DB;
365         $count = $DB->count_records_select('grade_grades',
366                                            'itemid = :gradeitemid AND finalgrade IS NOT NULL',
367                                            array('gradeitemid' => $this->id));
368         return $count > 0;
369     }
371     /**
372      * Check to see if there are existing overridden grades for this grade_item.
373      *
374      * @return boolean - true if there are overridden grades for this grade_item.
375      */
376     public function has_overridden_grades() {
377         global $DB;
379         $count = $DB->count_records_select('grade_grades',
380                                            'itemid = :gradeitemid AND finalgrade IS NOT NULL AND overridden > 0',
381                                            array('gradeitemid' => $this->id));
382         return $count > 0;
383     }
385     /**
386      * Finds and returns all grade_item instances based on params.
387      *
388      * @static
389      * @param array $params associative arrays varname=>value
390      * @return array array of grade_item instances or false if none found.
391      */
392     public static function fetch_all($params) {
393         return grade_object::fetch_all_helper('grade_items', 'grade_item', $params);
394     }
396     /**
397      * Delete all grades and force_regrading of parent category.
398      *
399      * @param string $source from where was the object deleted (mod/forum, manual, etc.)
400      * @return bool success
401      */
402     public function delete($source=null) {
403         $this->delete_all_grades($source);
404         return parent::delete($source);
405     }
407     /**
408      * Delete all grades
409      *
410      * @param string $source from where was the object deleted (mod/forum, manual, etc.)
411      * @return bool
412      */
413     public function delete_all_grades($source=null) {
414         if (!$this->is_course_item()) {
415             $this->force_regrading();
416         }
418         if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
419             foreach ($grades as $grade) {
420                 $grade->delete($source);
421             }
422         }
424         return true;
425     }
427     /**
428      * In addition to perform parent::insert(), calls force_regrading() method too.
429      *
430      * @param string $source From where was the object inserted (mod/forum, manual, etc.)
431      * @return int PK ID if successful, false otherwise
432      */
433     public function insert($source=null) {
434         global $CFG, $DB;
436         if (empty($this->courseid)) {
437             print_error('cannotinsertgrade');
438         }
440         // load scale if needed
441         $this->load_scale();
443         // add parent category if needed
444         if (empty($this->categoryid) and !$this->is_course_item() and !$this->is_category_item()) {
445             $course_category = grade_category::fetch_course_category($this->courseid);
446             $this->categoryid = $course_category->id;
448         }
450         // always place the new items at the end, move them after insert if needed
451         $last_sortorder = $DB->get_field_select('grade_items', 'MAX(sortorder)', "courseid = ?", array($this->courseid));
452         if (!empty($last_sortorder)) {
453             $this->sortorder = $last_sortorder + 1;
454         } else {
455             $this->sortorder = 1;
456         }
458         // add proper item numbers to manual items
459         if ($this->itemtype == 'manual') {
460             if (empty($this->itemnumber)) {
461                 $this->itemnumber = 0;
462             }
463         }
465         // make sure there is not 0 in outcomeid
466         if (empty($this->outcomeid)) {
467             $this->outcomeid = null;
468         }
470         $this->timecreated = $this->timemodified = time();
472         if (parent::insert($source)) {
473             // force regrading of items if needed
474             $this->force_regrading();
475             return $this->id;
477         } else {
478             debugging("Could not insert this grade_item in the database!");
479             return false;
480         }
481     }
483     /**
484      * Set idnumber of grade item, updates also course_modules table
485      *
486      * @param string $idnumber (without magic quotes)
487      * @return bool success
488      */
489     public function add_idnumber($idnumber) {
490         global $DB;
491         if (!empty($this->idnumber)) {
492             return false;
493         }
495         if ($this->itemtype == 'mod' and !$this->is_outcome_item()) {
496             if ($this->itemnumber == 0) {
497                 // for activity modules, itemnumber 0 is synced with the course_modules
498                 if (!$cm = get_coursemodule_from_instance($this->itemmodule, $this->iteminstance, $this->courseid)) {
499                     return false;
500                 }
501                 if (!empty($cm->idnumber)) {
502                     return false;
503                 }
504                 $DB->set_field('course_modules', 'idnumber', $idnumber, array('id' => $cm->id));
505                 $this->idnumber = $idnumber;
506                 return $this->update();
507             } else {
508                 $this->idnumber = $idnumber;
509                 return $this->update();
510             }
512         } else {
513             $this->idnumber = $idnumber;
514             return $this->update();
515         }
516     }
518     /**
519      * Returns the locked state of this grade_item (if the grade_item is locked OR no specific
520      * $userid is given) or the locked state of a specific grade within this item if a specific
521      * $userid is given and the grade_item is unlocked.
522      *
523      * @param int $userid The user's ID
524      * @return bool Locked state
525      */
526     public function is_locked($userid=NULL) {
527         if (!empty($this->locked)) {
528             return true;
529         }
531         if (!empty($userid)) {
532             if ($grade = grade_grade::fetch(array('itemid'=>$this->id, 'userid'=>$userid))) {
533                 $grade->grade_item =& $this; // prevent db fetching of cached grade_item
534                 return $grade->is_locked();
535             }
536         }
538         return false;
539     }
541     /**
542      * Locks or unlocks this grade_item and (optionally) all its associated final grades.
543      *
544      * @param int $lockedstate 0, 1 or a timestamp int(10) after which date the item will be locked.
545      * @param bool $cascade Lock/unlock child objects too
546      * @param bool $refresh Refresh grades when unlocking
547      * @return bool True if grade_item all grades updated, false if at least one update fails
548      */
549     public function set_locked($lockedstate, $cascade=false, $refresh=true) {
550         if ($lockedstate) {
551         /// setting lock
552             if ($this->needsupdate) {
553                 return false; // can not lock grade without first having final grade
554             }
556             $this->locked = time();
557             $this->update();
559             if ($cascade) {
560                 $grades = $this->get_final();
561                 foreach($grades as $g) {
562                     $grade = new grade_grade($g, false);
563                     $grade->grade_item =& $this;
564                     $grade->set_locked(1, null, false);
565                 }
566             }
568             return true;
570         } else {
571         /// removing lock
572             if (!empty($this->locked) and $this->locktime < time()) {
573                 //we have to reset locktime or else it would lock up again
574                 $this->locktime = 0;
575             }
577             $this->locked = 0;
578             $this->update();
580             if ($cascade) {
581                 if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
582                     foreach($grades as $grade) {
583                         $grade->grade_item =& $this;
584                         $grade->set_locked(0, null, false);
585                     }
586                 }
587             }
589             if ($refresh) {
590                 //refresh when unlocking
591                 $this->refresh_grades();
592             }
594             return true;
595         }
596     }
598     /**
599      * Lock the grade if needed. Make sure this is called only when final grades are valid
600      */
601     public function check_locktime() {
602         if (!empty($this->locked)) {
603             return; // already locked
604         }
606         if ($this->locktime and $this->locktime < time()) {
607             $this->locked = time();
608             $this->update('locktime');
609         }
610     }
612     /**
613      * Set the locktime for this grade item.
614      *
615      * @param int $locktime timestamp for lock to activate
616      * @return void
617      */
618     public function set_locktime($locktime) {
619         $this->locktime = $locktime;
620         $this->update();
621     }
623     /**
624      * Set the locktime for this grade item.
625      *
626      * @return int $locktime timestamp for lock to activate
627      */
628     public function get_locktime() {
629         return $this->locktime;
630     }
632     /**
633      * Set the hidden status of grade_item and all grades.
634      *
635      * 0 mean always visible, 1 means always hidden and a number > 1 is a timestamp to hide until
636      *
637      * @param int $hidden new hidden status
638      * @param bool $cascade apply to child objects too
639      */
640     public function set_hidden($hidden, $cascade=false) {
641         parent::set_hidden($hidden, $cascade);
643         if ($cascade) {
644             if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
645                 foreach($grades as $grade) {
646                     $grade->grade_item =& $this;
647                     $grade->set_hidden($hidden, $cascade);
648                 }
649             }
650         }
652         //if marking item visible make sure category is visible MDL-21367
653         if( !$hidden ) {
654             $category_array = grade_category::fetch_all(array('id'=>$this->categoryid));
655             if ($category_array && array_key_exists($this->categoryid, $category_array)) {
656                 $category = $category_array[$this->categoryid];
657                 //call set_hidden on the category regardless of whether it is hidden as its parent might be hidden
658                 //if($category->is_hidden()) {
659                     $category->set_hidden($hidden, false);
660                 //}
661             }
662         }
663     }
665     /**
666      * Returns the number of grades that are hidden
667      *
668      * @param string $groupsql SQL to limit the query by group
669      * @param array $params SQL params for $groupsql
670      * @param string $groupwheresql Where conditions for $groupsql
671      * @return int The number of hidden grades
672      */
673     public function has_hidden_grades($groupsql="", array $params=null, $groupwheresql="") {
674         global $DB;
675         $params = (array)$params;
676         $params['itemid'] = $this->id;
678         return $DB->get_field_sql("SELECT COUNT(*) FROM {grade_grades} g LEFT JOIN "
679                             ."{user} u ON g.userid = u.id $groupsql WHERE itemid = :itemid AND hidden = 1 $groupwheresql", $params);
680     }
682     /**
683      * Mark regrading as finished successfully.
684      */
685     public function regrading_finished() {
686         global $DB;
687         $this->needsupdate = 0;
688         //do not use $this->update() because we do not want this logged in grade_item_history
689         $DB->set_field('grade_items', 'needsupdate', 0, array('id' => $this->id));
690     }
692     /**
693      * Performs the necessary calculations on the grades_final referenced by this grade_item.
694      * Also resets the needsupdate flag once successfully performed.
695      *
696      * This function must be used ONLY from lib/gradeslib.php/grade_regrade_final_grades(),
697      * because the regrading must be done in correct order!!
698      *
699      * @param int $userid Supply a user ID to limit the regrading to a single user
700      * @return bool true if ok, error string otherwise
701      */
702     public function regrade_final_grades($userid=null) {
703         global $CFG, $DB;
705         // locked grade items already have correct final grades
706         if ($this->is_locked()) {
707             return true;
708         }
710         // calculation produces final value using formula from other final values
711         if ($this->is_calculated()) {
712             if ($this->compute($userid)) {
713                 return true;
714             } else {
715                 return "Could not calculate grades for grade item"; // TODO: improve and localize
716             }
718         // noncalculated outcomes already have final values - raw grades not used
719         } else if ($this->is_outcome_item()) {
720             return true;
722         // aggregate the category grade
723         } else if ($this->is_category_item() or $this->is_course_item()) {
724             // aggregate category grade item
725             $category = $this->load_item_category();
726             $category->grade_item =& $this;
727             if ($category->generate_grades($userid)) {
728                 return true;
729             } else {
730                 return "Could not aggregate final grades for category:".$this->id; // TODO: improve and localize
731             }
733         } else if ($this->is_manual_item()) {
734             // manual items track only final grades, no raw grades
735             return true;
737         } else if (!$this->is_raw_used()) {
738             // hmm - raw grades are not used- nothing to regrade
739             return true;
740         }
742         // normal grade item - just new final grades
743         $result = true;
744         $grade_inst = new grade_grade();
745         $fields = implode(',', $grade_inst->required_fields);
746         if ($userid) {
747             $params = array($this->id, $userid);
748             $rs = $DB->get_recordset_select('grade_grades', "itemid=? AND userid=?", $params, '', $fields);
749         } else {
750             $rs = $DB->get_recordset('grade_grades', array('itemid' => $this->id), '', $fields);
751         }
752         if ($rs) {
753             foreach ($rs as $grade_record) {
754                 $grade = new grade_grade($grade_record, false);
756                 if (!empty($grade_record->locked) or !empty($grade_record->overridden)) {
757                     // this grade is locked - final grade must be ok
758                     continue;
759                 }
761                 $grade->finalgrade = $this->adjust_raw_grade($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
763                 if (grade_floats_different($grade_record->finalgrade, $grade->finalgrade)) {
764                     $success = $grade->update('system');
766                     // If successful trigger a user_graded event.
767                     if ($success) {
768                         $grade->load_grade_item();
769                         \core\event\user_graded::create_from_grade($grade)->trigger();
770                     } else {
771                         $result = "Internal error updating final grade";
772                     }
773                 }
774             }
775             $rs->close();
776         }
778         return $result;
779     }
781     /**
782      * Given a float grade value or integer grade scale, applies a number of adjustment based on
783      * grade_item variables and returns the result.
784      *
785      * @param float $rawgrade The raw grade value
786      * @param float $rawmin original rawmin
787      * @param float $rawmax original rawmax
788      * @return mixed
789      */
790     public function adjust_raw_grade($rawgrade, $rawmin, $rawmax) {
791         if (is_null($rawgrade)) {
792             return null;
793         }
795         if ($this->gradetype == GRADE_TYPE_VALUE) { // Dealing with numerical grade
797             if ($this->grademax < $this->grademin) {
798                 return null;
799             }
801             if ($this->grademax == $this->grademin) {
802                 return $this->grademax; // no range
803             }
805             // Standardise score to the new grade range
806             // NOTE: skip if the activity provides a manual rescaling option.
807             $manuallyrescale = (component_callback_exists('mod_' . $this->itemmodule, 'rescale_activity_grades') !== false);
808             if (!$manuallyrescale && ($rawmin != $this->grademin or $rawmax != $this->grademax)) {
809                 $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax);
810             }
812             // Apply other grade_item factors
813             $rawgrade *= $this->multfactor;
814             $rawgrade += $this->plusfactor;
816             return $this->bounded_grade($rawgrade);
818         } else if ($this->gradetype == GRADE_TYPE_SCALE) { // Dealing with a scale value
819             if (empty($this->scale)) {
820                 $this->load_scale();
821             }
823             if ($this->grademax < 0) {
824                 return null; // scale not present - no grade
825             }
827             if ($this->grademax == 0) {
828                 return $this->grademax; // only one option
829             }
831             // Convert scale if needed
832             // NOTE: skip if the activity provides a manual rescaling option.
833             $manuallyrescale = (component_callback_exists('mod_' . $this->itemmodule, 'rescale_activity_grades') !== false);
834             if (!$manuallyrescale && ($rawmin != $this->grademin or $rawmax != $this->grademax)) {
835                 // This should never happen because scales are locked if they are in use.
836                 $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax);
837             }
839             return $this->bounded_grade($rawgrade);
842         } else if ($this->gradetype == GRADE_TYPE_TEXT or $this->gradetype == GRADE_TYPE_NONE) { // no value
843             // somebody changed the grading type when grades already existed
844             return null;
846         } else {
847             debugging("Unknown grade type");
848             return null;
849         }
850     }
852     /**
853      * Update the rawgrademax and rawgrademin for all grade_grades records for this item.
854      * Scale every rawgrade to maintain the percentage. This function should be called
855      * after the gradeitem has been updated to the new min and max values.
856      *
857      * @param float $oldgrademin The previous grade min value
858      * @param float $oldgrademax The previous grade max value
859      * @param float $newgrademin The new grade min value
860      * @param float $newgrademax The new grade max value
861      * @param string $source from where was the object inserted (mod/forum, manual, etc.)
862      * @return bool True on success
863      */
864     public function rescale_grades_keep_percentage($oldgrademin, $oldgrademax, $newgrademin, $newgrademax, $source = null) {
865         global $DB;
867         if (empty($this->id)) {
868             return false;
869         }
871         if ($oldgrademax <= $oldgrademin) {
872             // Grades cannot be scaled.
873             return false;
874         }
875         $scale = ($newgrademax - $newgrademin) / ($oldgrademax - $oldgrademin);
876         if (($newgrademax - $newgrademin) <= 1) {
877             // We would lose too much precision, lets bail.
878             return false;
879         }
881         $rs = $DB->get_recordset('grade_grades', array('itemid' => $this->id));
883         foreach ($rs as $graderecord) {
884             // For each record, create an object to work on.
885             $grade = new grade_grade($graderecord, false);
886             // Set this object in the item so it doesn't re-fetch it.
887             $grade->grade_item = $this;
889             if (!$this->is_category_item() || ($this->is_category_item() && $grade->is_overridden())) {
890                 // Updating the raw grade automatically updates the min/max.
891                 if ($this->is_raw_used()) {
892                     $rawgrade = (($grade->rawgrade - $oldgrademin) * $scale) + $newgrademin;
893                     $this->update_raw_grade(false, $rawgrade, $source, false, FORMAT_MOODLE, null, null, null, $grade);
894                 } else {
895                     $finalgrade = (($grade->finalgrade - $oldgrademin) * $scale) + $newgrademin;
896                     $this->update_final_grade($grade->userid, $finalgrade, $source);
897                 }
898             }
899         }
900         $rs->close();
902         // Mark this item for regrading.
903         $this->force_regrading();
905         return true;
906     }
908     /**
909      * Sets this grade_item's needsupdate to true. Also marks the course item as needing update.
910      *
911      * @return void
912      */
913     public function force_regrading() {
914         global $DB;
915         $this->needsupdate = 1;
916         //mark this item and course item only - categories and calculated items are always regraded
917         $wheresql = "(itemtype='course' OR id=?) AND courseid=?";
918         $params   = array($this->id, $this->courseid);
919         $DB->set_field_select('grade_items', 'needsupdate', 1, $wheresql, $params);
920     }
922     /**
923      * Instantiates a grade_scale object from the DB if this item's scaleid variable is set
924      *
925      * @return grade_scale Returns a grade_scale object or null if no scale used
926      */
927     public function load_scale() {
928         if ($this->gradetype != GRADE_TYPE_SCALE) {
929             $this->scaleid = null;
930         }
932         if (!empty($this->scaleid)) {
933             //do not load scale if already present
934             if (empty($this->scale->id) or $this->scale->id != $this->scaleid) {
935                 $this->scale = grade_scale::fetch(array('id'=>$this->scaleid));
936                 if (!$this->scale) {
937                     debugging('Incorrect scale id: '.$this->scaleid);
938                     $this->scale = null;
939                     return null;
940                 }
941                 $this->scale->load_items();
942             }
944             // Until scales are uniformly set to min=0 max=count(scaleitems)-1 throughout Moodle, we
945             // stay with the current min=1 max=count(scaleitems)
946             $this->grademax = count($this->scale->scale_items);
947             $this->grademin = 1;
949         } else {
950             $this->scale = null;
951         }
953         return $this->scale;
954     }
956     /**
957      * Instantiates a grade_outcome object from the DB if this item's outcomeid variable is set
958      *
959      * @return grade_outcome This grade item's associated grade_outcome or null
960      */
961     public function load_outcome() {
962         if (!empty($this->outcomeid)) {
963             $this->outcome = grade_outcome::fetch(array('id'=>$this->outcomeid));
964         }
965         return $this->outcome;
966     }
968     /**
969      * Returns the grade_category object this grade_item belongs to (referenced by categoryid)
970      * or category attached to category item.
971      *
972      * @return grade_category|bool Returns a grade_category object if applicable or false if this is a course item
973      */
974     public function get_parent_category() {
975         if ($this->is_category_item() or $this->is_course_item()) {
976             return $this->get_item_category();
978         } else {
979             return grade_category::fetch(array('id'=>$this->categoryid));
980         }
981     }
983     /**
984      * Calls upon the get_parent_category method to retrieve the grade_category object
985      * from the DB and assigns it to $this->parent_category. It also returns the object.
986      *
987      * @return grade_category This grade item's parent grade_category.
988      */
989     public function load_parent_category() {
990         if (empty($this->parent_category->id)) {
991             $this->parent_category = $this->get_parent_category();
992         }
993         return $this->parent_category;
994     }
996     /**
997      * Returns the grade_category for a grade category grade item
998      *
999      * @return grade_category|bool Returns a grade_category instance if applicable or false otherwise
1000      */
1001     public function get_item_category() {
1002         if (!$this->is_course_item() and !$this->is_category_item()) {
1003             return false;
1004         }
1005         return grade_category::fetch(array('id'=>$this->iteminstance));
1006     }
1008     /**
1009      * Calls upon the get_item_category method to retrieve the grade_category object
1010      * from the DB and assigns it to $this->item_category. It also returns the object.
1011      *
1012      * @return grade_category
1013      */
1014     public function load_item_category() {
1015         if (empty($this->item_category->id)) {
1016             $this->item_category = $this->get_item_category();
1017         }
1018         return $this->item_category;
1019     }
1021     /**
1022      * Is the grade item associated with category?
1023      *
1024      * @return bool
1025      */
1026     public function is_category_item() {
1027         return ($this->itemtype == 'category');
1028     }
1030     /**
1031      * Is the grade item associated with course?
1032      *
1033      * @return bool
1034      */
1035     public function is_course_item() {
1036         return ($this->itemtype == 'course');
1037     }
1039     /**
1040      * Is this a manually graded item?
1041      *
1042      * @return bool
1043      */
1044     public function is_manual_item() {
1045         return ($this->itemtype == 'manual');
1046     }
1048     /**
1049      * Is this an outcome item?
1050      *
1051      * @return bool
1052      */
1053     public function is_outcome_item() {
1054         return !empty($this->outcomeid);
1055     }
1057     /**
1058      * Is the grade item external - associated with module, plugin or something else?
1059      *
1060      * @return bool
1061      */
1062     public function is_external_item() {
1063         return ($this->itemtype == 'mod');
1064     }
1066     /**
1067      * Is the grade item overridable
1068      *
1069      * @return bool
1070      */
1071     public function is_overridable_item() {
1072         if ($this->is_course_item() or $this->is_category_item()) {
1073             $overridable = (bool) get_config('moodle', 'grade_overridecat');
1074         } else {
1075             $overridable = false;
1076         }
1078         return !$this->is_outcome_item() and ($this->is_external_item() or $this->is_calculated() or $overridable);
1079     }
1081     /**
1082      * Is the grade item feedback overridable
1083      *
1084      * @return bool
1085      */
1086     public function is_overridable_item_feedback() {
1087         return !$this->is_outcome_item() and $this->is_external_item();
1088     }
1090     /**
1091      * Returns true if grade items uses raw grades
1092      *
1093      * @return bool
1094      */
1095     public function is_raw_used() {
1096         return ($this->is_external_item() and !$this->is_calculated() and !$this->is_outcome_item());
1097     }
1099     /**
1100      * Returns true if the grade item is an aggreggated type grade.
1101      *
1102      * @since  Moodle 2.8.7, 2.9.1
1103      * @return bool
1104      */
1105     public function is_aggregate_item() {
1106         return ($this->is_category_item() || $this->is_course_item());
1107     }
1109     /**
1110      * Returns the grade item associated with the course
1111      *
1112      * @param int $courseid
1113      * @return grade_item Course level grade item object
1114      */
1115     public static function fetch_course_item($courseid) {
1116         if ($course_item = grade_item::fetch(array('courseid'=>$courseid, 'itemtype'=>'course'))) {
1117             return $course_item;
1118         }
1120         // first get category - it creates the associated grade item
1121         $course_category = grade_category::fetch_course_category($courseid);
1122         return $course_category->get_grade_item();
1123     }
1125     /**
1126      * Is grading object editable?
1127      *
1128      * @return bool
1129      */
1130     public function is_editable() {
1131         return true;
1132     }
1134     /**
1135      * Checks if grade calculated. Returns this object's calculation.
1136      *
1137      * @return bool true if grade item calculated.
1138      */
1139     public function is_calculated() {
1140         if (empty($this->calculation)) {
1141             return false;
1142         }
1144         /*
1145          * The main reason why we use the ##gixxx## instead of [[idnumber]] is speed of depends_on(),
1146          * we would have to fetch all course grade items to find out the ids.
1147          * Also if user changes the idnumber the formula does not need to be updated.
1148          */
1150         // first detect if we need to change calculation formula from [[idnumber]] to ##giXXX## (after backup, etc.)
1151         if (!$this->calculation_normalized and strpos($this->calculation, '[[') !== false) {
1152             $this->set_calculation($this->calculation);
1153         }
1155         return !empty($this->calculation);
1156     }
1158     /**
1159      * Returns calculation string if grade calculated.
1160      *
1161      * @return string Returns the grade item's calculation if calculation is used, null if not
1162      */
1163     public function get_calculation() {
1164         if ($this->is_calculated()) {
1165             return grade_item::denormalize_formula($this->calculation, $this->courseid);
1167         } else {
1168             return NULL;
1169         }
1170     }
1172     /**
1173      * Sets this item's calculation (creates it) if not yet set, or
1174      * updates it if already set (in the DB). If no calculation is given,
1175      * the calculation is removed.
1176      *
1177      * @param string $formula string representation of formula used for calculation
1178      * @return bool success
1179      */
1180     public function set_calculation($formula) {
1181         $this->calculation = grade_item::normalize_formula($formula, $this->courseid);
1182         $this->calculation_normalized = true;
1183         return $this->update();
1184     }
1186     /**
1187      * Denormalizes the calculation formula to [idnumber] form
1188      *
1189      * @param string $formula A string representation of the formula
1190      * @param int $courseid The course ID
1191      * @return string The denormalized formula as a string
1192      */
1193     public static function denormalize_formula($formula, $courseid) {
1194         if (empty($formula)) {
1195             return '';
1196         }
1198         // denormalize formula - convert ##giXX## to [[idnumber]]
1199         if (preg_match_all('/##gi(\d+)##/', $formula, $matches)) {
1200             foreach ($matches[1] as $id) {
1201                 if ($grade_item = grade_item::fetch(array('id'=>$id, 'courseid'=>$courseid))) {
1202                     if (!empty($grade_item->idnumber)) {
1203                         $formula = str_replace('##gi'.$grade_item->id.'##', '[['.$grade_item->idnumber.']]', $formula);
1204                     }
1205                 }
1206             }
1207         }
1209         return $formula;
1211     }
1213     /**
1214      * Normalizes the calculation formula to [#giXX#] form
1215      *
1216      * @param string $formula The formula
1217      * @param int $courseid The course ID
1218      * @return string The normalized formula as a string
1219      */
1220     public static function normalize_formula($formula, $courseid) {
1221         $formula = trim($formula);
1223         if (empty($formula)) {
1224             return NULL;
1226         }
1228         // normalize formula - we want grade item ids ##giXXX## instead of [[idnumber]]
1229         if ($grade_items = grade_item::fetch_all(array('courseid'=>$courseid))) {
1230             foreach ($grade_items as $grade_item) {
1231                 $formula = str_replace('[['.$grade_item->idnumber.']]', '##gi'.$grade_item->id.'##', $formula);
1232             }
1233         }
1235         return $formula;
1236     }
1238     /**
1239      * Returns the final values for this grade item (as imported by module or other source).
1240      *
1241      * @param int $userid Optional: to retrieve a single user's final grade
1242      * @return array|grade_grade An array of all grade_grade instances for this grade_item, or a single grade_grade instance.
1243      */
1244     public function get_final($userid=NULL) {
1245         global $DB;
1246         if ($userid) {
1247             if ($user = $DB->get_record('grade_grades', array('itemid' => $this->id, 'userid' => $userid))) {
1248                 return $user;
1249             }
1251         } else {
1252             if ($grades = $DB->get_records('grade_grades', array('itemid' => $this->id))) {
1253                 //TODO: speed up with better SQL (MDL-31380)
1254                 $result = array();
1255                 foreach ($grades as $grade) {
1256                     $result[$grade->userid] = $grade;
1257                 }
1258                 return $result;
1259             } else {
1260                 return array();
1261             }
1262         }
1263     }
1265     /**
1266      * Get (or create if not exist yet) grade for this user
1267      *
1268      * @param int $userid The user ID
1269      * @param bool $create If true and the user has no grade for this grade item a new grade_grade instance will be inserted
1270      * @return grade_grade The grade_grade instance for the user for this grade item
1271      */
1272     public function get_grade($userid, $create=true) {
1273         if (empty($this->id)) {
1274             debugging('Can not use before insert');
1275             return false;
1276         }
1278         $grade = new grade_grade(array('userid'=>$userid, 'itemid'=>$this->id));
1279         if (empty($grade->id) and $create) {
1280             $grade->insert();
1281         }
1283         return $grade;
1284     }
1286     /**
1287      * Returns the sortorder of this grade_item. This method is also available in
1288      * grade_category, for cases where the object type is not know.
1289      *
1290      * @return int Sort order
1291      */
1292     public function get_sortorder() {
1293         return $this->sortorder;
1294     }
1296     /**
1297      * Returns the idnumber of this grade_item. This method is also available in
1298      * grade_category, for cases where the object type is not know.
1299      *
1300      * @return string The grade item idnumber
1301      */
1302     public function get_idnumber() {
1303         return $this->idnumber;
1304     }
1306     /**
1307      * Returns this grade_item. This method is also available in
1308      * grade_category, for cases where the object type is not know.
1309      *
1310      * @return grade_item
1311      */
1312     public function get_grade_item() {
1313         return $this;
1314     }
1316     /**
1317      * Sets the sortorder of this grade_item. This method is also available in
1318      * grade_category, for cases where the object type is not know.
1319      *
1320      * @param int $sortorder
1321      */
1322     public function set_sortorder($sortorder) {
1323         if ($this->sortorder == $sortorder) {
1324             return;
1325         }
1326         $this->sortorder = $sortorder;
1327         $this->update();
1328     }
1330     /**
1331      * Update this grade item's sortorder so that it will appear after $sortorder
1332      *
1333      * @param int $sortorder The sort order to place this grade item after
1334      */
1335     public function move_after_sortorder($sortorder) {
1336         global $CFG, $DB;
1338         //make some room first
1339         $params = array($sortorder, $this->courseid);
1340         $sql = "UPDATE {grade_items}
1341                    SET sortorder = sortorder + 1
1342                  WHERE sortorder > ? AND courseid = ?";
1343         $DB->execute($sql, $params);
1345         $this->set_sortorder($sortorder + 1);
1346     }
1348     /**
1349      * Detect duplicate grade item's sortorder and re-sort them.
1350      * Note: Duplicate sortorder will be introduced while duplicating activities or
1351      * merging two courses.
1352      *
1353      * @param int $courseid id of the course for which grade_items sortorder need to be fixed.
1354      */
1355     public static function fix_duplicate_sortorder($courseid) {
1356         global $DB;
1358         $transaction = $DB->start_delegated_transaction();
1360         $sql = "SELECT DISTINCT g1.id, g1.courseid, g1.sortorder
1361                     FROM {grade_items} g1
1362                     JOIN {grade_items} g2 ON g1.courseid = g2.courseid
1363                 WHERE g1.sortorder = g2.sortorder AND g1.id != g2.id AND g1.courseid = :courseid
1364                 ORDER BY g1.sortorder DESC, g1.id DESC";
1366         // Get all duplicates in course highest sort order, and higest id first so that we can make space at the
1367         // bottom higher end of the sort orders and work down by id.
1368         $rs = $DB->get_recordset_sql($sql, array('courseid' => $courseid));
1370         foreach($rs as $duplicate) {
1371             $DB->execute("UPDATE {grade_items}
1372                             SET sortorder = sortorder + 1
1373                           WHERE courseid = :courseid AND
1374                           (sortorder > :sortorder OR (sortorder = :sortorder2 AND id > :id))",
1375                 array('courseid' => $duplicate->courseid,
1376                     'sortorder' => $duplicate->sortorder,
1377                     'sortorder2' => $duplicate->sortorder,
1378                     'id' => $duplicate->id));
1379         }
1380         $rs->close();
1381         $transaction->allow_commit();
1382     }
1384     /**
1385      * Returns the most descriptive field for this object.
1386      *
1387      * Determines what type of grade item it is then returns the appropriate string
1388      *
1389      * @param bool $fulltotal If the item is a category total, returns $categoryname."total" instead of "Category total" or "Course total"
1390      * @return string name
1391      */
1392     public function get_name($fulltotal=false) {
1393         if (strval($this->itemname) !== '') {
1394             // MDL-10557
1395             return format_string($this->itemname);
1397         } else if ($this->is_course_item()) {
1398             return get_string('coursetotal', 'grades');
1400         } else if ($this->is_category_item()) {
1401             if ($fulltotal) {
1402                 $category = $this->load_parent_category();
1403                 $a = new stdClass();
1404                 $a->category = $category->get_name();
1405                 return get_string('categorytotalfull', 'grades', $a);
1406             } else {
1407             return get_string('categorytotal', 'grades');
1408             }
1410         } else {
1411             return get_string('grade');
1412         }
1413     }
1415     /**
1416      * A grade item can return a more detailed description which will be added to the header of the column/row in some reports.
1417      *
1418      * @return string description
1419      */
1420     public function get_description() {
1421         if ($this->is_course_item() || $this->is_category_item()) {
1422             $categoryitem = $this->load_item_category();
1423             return $categoryitem->get_description();
1424         }
1425         return '';
1426     }
1428     /**
1429      * Sets this item's categoryid. A generic method shared by objects that have a parent id of some kind.
1430      *
1431      * @param int $parentid The ID of the new parent
1432      * @param bool $updateaggregationfields Whether or not to convert the aggregation fields when switching between category.
1433      *                          Set this to false when the aggregation fields have been updated in prevision of the new
1434      *                          category, typically when the item is freshly created.
1435      * @return bool True if success
1436      */
1437     public function set_parent($parentid, $updateaggregationfields = true) {
1438         if ($this->is_course_item() or $this->is_category_item()) {
1439             print_error('cannotsetparentforcatoritem');
1440         }
1442         if ($this->categoryid == $parentid) {
1443             return true;
1444         }
1446         // find parent and check course id
1447         if (!$parent_category = grade_category::fetch(array('id'=>$parentid, 'courseid'=>$this->courseid))) {
1448             return false;
1449         }
1451         $currentparent = $this->load_parent_category();
1453         if ($updateaggregationfields) {
1454             $this->set_aggregation_fields_for_aggregation($currentparent->aggregation, $parent_category->aggregation);
1455         }
1457         $this->force_regrading();
1459         // set new parent
1460         $this->categoryid = $parent_category->id;
1461         $this->parent_category =& $parent_category;
1463         return $this->update();
1464     }
1466     /**
1467      * Update the aggregation fields when the aggregation changed.
1468      *
1469      * This method should always be called when the aggregation has changed, but also when
1470      * the item was moved to another category, even it if uses the same aggregation method.
1471      *
1472      * Some values such as the weight only make sense within a category, once moved the
1473      * values should be reset to let the user adapt them accordingly.
1474      *
1475      * Note that this method does not save the grade item.
1476      * {@link grade_item::update()} has to be called manually after using this method.
1477      *
1478      * @param  int $from Aggregation method constant value.
1479      * @param  int $to   Aggregation method constant value.
1480      * @return boolean   True when at least one field was changed, false otherwise
1481      */
1482     public function set_aggregation_fields_for_aggregation($from, $to) {
1483         $defaults = grade_category::get_default_aggregation_coefficient_values($to);
1485         $origaggregationcoef = $this->aggregationcoef;
1486         $origaggregationcoef2 = $this->aggregationcoef2;
1487         $origweighoverride = $this->weightoverride;
1489         if ($from == GRADE_AGGREGATE_SUM && $to == GRADE_AGGREGATE_SUM && $this->weightoverride) {
1490             // Do nothing. We are switching from SUM to SUM and the weight is overriden,
1491             // a teacher would not expect any change in this situation.
1493         } else if ($from == GRADE_AGGREGATE_WEIGHTED_MEAN && $to == GRADE_AGGREGATE_WEIGHTED_MEAN) {
1494             // Do nothing. The weights can be kept in this case.
1496         } else if (in_array($from, array(GRADE_AGGREGATE_SUM,  GRADE_AGGREGATE_EXTRACREDIT_MEAN, GRADE_AGGREGATE_WEIGHTED_MEAN2))
1497                 && in_array($to, array(GRADE_AGGREGATE_SUM,  GRADE_AGGREGATE_EXTRACREDIT_MEAN, GRADE_AGGREGATE_WEIGHTED_MEAN2))) {
1499             // Reset all but the the extra credit field.
1500             $this->aggregationcoef2 = $defaults['aggregationcoef2'];
1501             $this->weightoverride = $defaults['weightoverride'];
1503             if ($to != GRADE_AGGREGATE_EXTRACREDIT_MEAN) {
1504                 // Normalise extra credit, except for 'Mean with extra credit' which supports higher values than 1.
1505                 $this->aggregationcoef = min(1, $this->aggregationcoef);
1506             }
1507         } else {
1508             // Reset all.
1509             $this->aggregationcoef = $defaults['aggregationcoef'];
1510             $this->aggregationcoef2 = $defaults['aggregationcoef2'];
1511             $this->weightoverride = $defaults['weightoverride'];
1512         }
1514         $acoefdiff       = grade_floats_different($origaggregationcoef, $this->aggregationcoef);
1515         $acoefdiff2      = grade_floats_different($origaggregationcoef2, $this->aggregationcoef2);
1516         $weightoverride  = grade_floats_different($origweighoverride, $this->weightoverride);
1518         return $acoefdiff || $acoefdiff2 || $weightoverride;
1519     }
1521     /**
1522      * Makes sure value is a valid grade value.
1523      *
1524      * @param float $gradevalue
1525      * @return mixed float or int fixed grade value
1526      */
1527     public function bounded_grade($gradevalue) {
1528         global $CFG;
1530         if (is_null($gradevalue)) {
1531             return null;
1532         }
1534         if ($this->gradetype == GRADE_TYPE_SCALE) {
1535             // no >100% grades hack for scale grades!
1536             // 1.5 is rounded to 2 ;-)
1537             return (int)bounded_number($this->grademin, round($gradevalue+0.00001), $this->grademax);
1538         }
1540         $grademax = $this->grademax;
1542         // NOTE: if you change this value you must manually reset the needsupdate flag in all grade items
1543         $maxcoef = isset($CFG->gradeoverhundredprocentmax) ? $CFG->gradeoverhundredprocentmax : 10; // 1000% max by default
1545         if (!empty($CFG->unlimitedgrades)) {
1546             // NOTE: if you change this value you must manually reset the needsupdate flag in all grade items
1547             $grademax = $grademax * $maxcoef;
1548         } else if ($this->is_category_item() or $this->is_course_item()) {
1549             $category = $this->load_item_category();
1550             if ($category->aggregation >= 100) {
1551                 // grade >100% hack
1552                 $grademax = $grademax * $maxcoef;
1553             }
1554         }
1556         return (float)bounded_number($this->grademin, $gradevalue, $grademax);
1557     }
1559     /**
1560      * Finds out on which other items does this depend directly when doing calculation or category aggregation
1561      *
1562      * @param bool $reset_cache
1563      * @return array of grade_item IDs this one depends on
1564      */
1565     public function depends_on($reset_cache=false) {
1566         global $CFG, $DB;
1568         if ($reset_cache) {
1569             $this->dependson_cache = null;
1570         } else if (isset($this->dependson_cache)) {
1571             return $this->dependson_cache;
1572         }
1574         if ($this->is_locked()) {
1575             // locked items do not need to be regraded
1576             $this->dependson_cache = array();
1577             return $this->dependson_cache;
1578         }
1580         if ($this->is_calculated()) {
1581             if (preg_match_all('/##gi(\d+)##/', $this->calculation, $matches)) {
1582                 $this->dependson_cache = array_unique($matches[1]); // remove duplicates
1583                 return $this->dependson_cache;
1584             } else {
1585                 $this->dependson_cache = array();
1586                 return $this->dependson_cache;
1587             }
1589         } else if ($grade_category = $this->load_item_category()) {
1590             $params = array();
1592             //only items with numeric or scale values can be aggregated
1593             if ($this->gradetype != GRADE_TYPE_VALUE and $this->gradetype != GRADE_TYPE_SCALE) {
1594                 $this->dependson_cache = array();
1595                 return $this->dependson_cache;
1596             }
1598             $grade_category->apply_forced_settings();
1600             if (empty($CFG->enableoutcomes) or $grade_category->aggregateoutcomes) {
1601                 $outcomes_sql = "";
1602             } else {
1603                 $outcomes_sql = "AND gi.outcomeid IS NULL";
1604             }
1606             if (empty($CFG->grade_includescalesinaggregation)) {
1607                 $gtypes = "gi.gradetype = ?";
1608                 $params[] = GRADE_TYPE_VALUE;
1609             } else {
1610                 $gtypes = "(gi.gradetype = ? OR gi.gradetype = ?)";
1611                 $params[] = GRADE_TYPE_VALUE;
1612                 $params[] = GRADE_TYPE_SCALE;
1613             }
1615             $params[] = $grade_category->id;
1616             $params[] = $this->courseid;
1617             $params[] = $grade_category->id;
1618             $params[] = $this->courseid;
1619             if (empty($CFG->grade_includescalesinaggregation)) {
1620                 $params[] = GRADE_TYPE_VALUE;
1621             } else {
1622                 $params[] = GRADE_TYPE_VALUE;
1623                 $params[] = GRADE_TYPE_SCALE;
1624             }
1625             $sql = "SELECT gi.id
1626                       FROM {grade_items} gi
1627                      WHERE $gtypes
1628                            AND gi.categoryid = ?
1629                            AND gi.courseid = ?
1630                            $outcomes_sql
1631                     UNION
1633                     SELECT gi.id
1634                       FROM {grade_items} gi, {grade_categories} gc
1635                      WHERE (gi.itemtype = 'category' OR gi.itemtype = 'course') AND gi.iteminstance=gc.id
1636                            AND gc.parent = ?
1637                            AND gi.courseid = ?
1638                            AND $gtypes
1639                            $outcomes_sql";
1641             if ($children = $DB->get_records_sql($sql, $params)) {
1642                 $this->dependson_cache = array_keys($children);
1643                 return $this->dependson_cache;
1644             } else {
1645                 $this->dependson_cache = array();
1646                 return $this->dependson_cache;
1647             }
1649         } else {
1650             $this->dependson_cache = array();
1651             return $this->dependson_cache;
1652         }
1653     }
1655     /**
1656      * Refetch grades from modules, plugins.
1657      *
1658      * @param int $userid optional, limit the refetch to a single user
1659      * @return bool Returns true on success or if there is nothing to do
1660      */
1661     public function refresh_grades($userid=0) {
1662         global $DB;
1663         if ($this->itemtype == 'mod') {
1664             if ($this->is_outcome_item()) {
1665                 //nothing to do
1666                 return true;
1667             }
1669             if (!$activity = $DB->get_record($this->itemmodule, array('id' => $this->iteminstance))) {
1670                 debugging("Can not find $this->itemmodule activity with id $this->iteminstance");
1671                 return false;
1672             }
1674             if (!$cm = get_coursemodule_from_instance($this->itemmodule, $activity->id, $this->courseid)) {
1675                 debugging('Can not find course module');
1676                 return false;
1677             }
1679             $activity->modname    = $this->itemmodule;
1680             $activity->cmidnumber = $cm->idnumber;
1682             return grade_update_mod_grades($activity, $userid);
1683         }
1685         return true;
1686     }
1688     /**
1689      * Updates final grade value for given user, this is a only way to update final
1690      * grades from gradebook and import because it logs the change in history table
1691      * and deals with overridden flag. This flag is set to prevent later overriding
1692      * from raw grades submitted from modules.
1693      *
1694      * @param int $userid The graded user
1695      * @param float|false $finalgrade The float value of final grade, false means do not change
1696      * @param string $source The modification source
1697      * @param string $feedback Optional teacher feedback
1698      * @param int $feedbackformat A format like FORMAT_PLAIN or FORMAT_HTML
1699      * @param int $usermodified The ID of the user making the modification
1700      * @return bool success
1701      */
1702     public function update_final_grade($userid, $finalgrade=false, $source=NULL, $feedback=false, $feedbackformat=FORMAT_MOODLE, $usermodified=null) {
1703         global $USER, $CFG;
1705         $result = true;
1707         // no grading used or locked
1708         if ($this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) {
1709             return false;
1710         }
1712         $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid));
1713         $grade->grade_item =& $this; // prevent db fetching of this grade_item
1715         if (empty($usermodified)) {
1716             $grade->usermodified = $USER->id;
1717         } else {
1718             $grade->usermodified = $usermodified;
1719         }
1721         if ($grade->is_locked()) {
1722             // do not update locked grades at all
1723             return false;
1724         }
1726         $locktime = $grade->get_locktime();
1727         if ($locktime and $locktime < time()) {
1728             // do not update grades that should be already locked, force regrade instead
1729             $this->force_regrading();
1730             return false;
1731         }
1733         $oldgrade = new stdClass();
1734         $oldgrade->finalgrade     = $grade->finalgrade;
1735         $oldgrade->overridden     = $grade->overridden;
1736         $oldgrade->feedback       = $grade->feedback;
1737         $oldgrade->feedbackformat = $grade->feedbackformat;
1738         $oldgrade->rawgrademin    = $grade->rawgrademin;
1739         $oldgrade->rawgrademax    = $grade->rawgrademax;
1741         // MDL-31713 rawgramemin and max must be up to date so conditional access %'s works properly.
1742         $grade->rawgrademin = $this->grademin;
1743         $grade->rawgrademax = $this->grademax;
1744         $grade->rawscaleid  = $this->scaleid;
1746         // changed grade?
1747         if ($finalgrade !== false) {
1748             if ($this->is_overridable_item()) {
1749                 $grade->overridden = time();
1750             }
1752             $grade->finalgrade = $this->bounded_grade($finalgrade);
1753         }
1755         // do we have comment from teacher?
1756         if ($feedback !== false) {
1757             if ($this->is_overridable_item_feedback()) {
1758                 // external items (modules, plugins) may have own feedback
1759                 $grade->overridden = time();
1760             }
1762             $grade->feedback       = $feedback;
1763             $grade->feedbackformat = $feedbackformat;
1764         }
1766         $gradechanged = false;
1767         if (empty($grade->id)) {
1768             $grade->timecreated  = null;   // hack alert - date submitted - no submission yet
1769             $grade->timemodified = time(); // hack alert - date graded
1770             $result = (bool)$grade->insert($source);
1772             // If the grade insert was successful and the final grade was not null then trigger a user_graded event.
1773             if ($result && !is_null($grade->finalgrade)) {
1774                 \core\event\user_graded::create_from_grade($grade)->trigger();
1775             }
1776             $gradechanged = true;
1777         } else {
1778             // Existing grade_grades.
1780             if (grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)
1781                     or grade_floats_different($grade->rawgrademin, $oldgrade->rawgrademin)
1782                     or grade_floats_different($grade->rawgrademax, $oldgrade->rawgrademax)
1783                     or ($oldgrade->overridden == 0 and $grade->overridden > 0)) {
1784                 $gradechanged = true;
1785             }
1787             if ($grade->feedback === $oldgrade->feedback and $grade->feedbackformat == $oldgrade->feedbackformat and
1788                     $gradechanged === false) {
1789                 // No grade nor feedback changed.
1790                 return $result;
1791             }
1793             $grade->timemodified = time(); // hack alert - date graded
1794             $result = $grade->update($source);
1796             // If the grade update was successful and the actual grade has changed then trigger a user_graded event.
1797             if ($result && grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)) {
1798                 \core\event\user_graded::create_from_grade($grade)->trigger();
1799             }
1800         }
1802         if (!$result) {
1803             // Something went wrong - better force final grade recalculation.
1804             $this->force_regrading();
1805             return $result;
1806         }
1808         // If we are not updating grades we don't need to recalculate the whole course.
1809         if (!$gradechanged) {
1810             return $result;
1811         }
1813         if ($this->is_course_item() and !$this->needsupdate) {
1814             if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
1815                 $this->force_regrading();
1816             }
1818         } else if (!$this->needsupdate) {
1820             $course_item = grade_item::fetch_course_item($this->courseid);
1821             if (!$course_item->needsupdate) {
1822                 if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
1823                     $this->force_regrading();
1824                 }
1825             } else {
1826                 $this->force_regrading();
1827             }
1828         }
1830         return $result;
1831     }
1834     /**
1835      * Updates raw grade value for given user, this is a only way to update raw
1836      * grades from external source (modules, etc.),
1837      * because it logs the change in history table and deals with final grade recalculation.
1838      *
1839      * @param int $userid the graded user
1840      * @param mixed $rawgrade float value of raw grade - false means do not change
1841      * @param string $source modification source
1842      * @param string $feedback optional teacher feedback
1843      * @param int $feedbackformat A format like FORMAT_PLAIN or FORMAT_HTML
1844      * @param int $usermodified the ID of the user who did the grading
1845      * @param int $dategraded A timestamp of when the student's work was graded
1846      * @param int $datesubmitted A timestamp of when the student's work was submitted
1847      * @param grade_grade $grade A grade object, useful for bulk upgrades
1848      * @return bool success
1849      */
1850     public function update_raw_grade($userid, $rawgrade=false, $source=NULL, $feedback=false, $feedbackformat=FORMAT_MOODLE, $usermodified=null, $dategraded=null, $datesubmitted=null, $grade=null) {
1851         global $USER;
1853         $result = true;
1855         // calculated grades can not be updated; course and category can not be updated  because they are aggregated
1856         if (!$this->is_raw_used() or $this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) {
1857             return false;
1858         }
1860         if (is_null($grade)) {
1861             //fetch from db
1862             $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid));
1863         }
1864         $grade->grade_item =& $this; // prevent db fetching of this grade_item
1866         if (empty($usermodified)) {
1867             $grade->usermodified = $USER->id;
1868         } else {
1869             $grade->usermodified = $usermodified;
1870         }
1872         if ($grade->is_locked()) {
1873             // do not update locked grades at all
1874             return false;
1875         }
1877         $locktime = $grade->get_locktime();
1878         if ($locktime and $locktime < time()) {
1879             // do not update grades that should be already locked and force regrade
1880             $this->force_regrading();
1881             return false;
1882         }
1884         $oldgrade = new stdClass();
1885         $oldgrade->finalgrade     = $grade->finalgrade;
1886         $oldgrade->rawgrade       = $grade->rawgrade;
1887         $oldgrade->rawgrademin    = $grade->rawgrademin;
1888         $oldgrade->rawgrademax    = $grade->rawgrademax;
1889         $oldgrade->rawscaleid     = $grade->rawscaleid;
1890         $oldgrade->feedback       = $grade->feedback;
1891         $oldgrade->feedbackformat = $grade->feedbackformat;
1893         // use new min and max
1894         $grade->rawgrade    = $grade->rawgrade;
1895         $grade->rawgrademin = $this->grademin;
1896         $grade->rawgrademax = $this->grademax;
1897         $grade->rawscaleid  = $this->scaleid;
1899         // change raw grade?
1900         if ($rawgrade !== false) {
1901             $grade->rawgrade = $rawgrade;
1902         }
1904         // empty feedback means no feedback at all
1905         if ($feedback === '') {
1906             $feedback = null;
1907         }
1909         // do we have comment from teacher?
1910         if ($feedback !== false and !$grade->is_overridden()) {
1911             $grade->feedback       = $feedback;
1912             $grade->feedbackformat = $feedbackformat;
1913         }
1915         // update final grade if possible
1916         if (!$grade->is_locked() and !$grade->is_overridden()) {
1917             $grade->finalgrade = $this->adjust_raw_grade($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
1918         }
1920         // TODO: hack alert - create new fields for these in 2.0
1921         $oldgrade->timecreated  = $grade->timecreated;
1922         $oldgrade->timemodified = $grade->timemodified;
1924         $grade->timecreated = $datesubmitted;
1926         if ($grade->is_overridden()) {
1927             // keep original graded date - update_final_grade() sets this for overridden grades
1929         } else if (is_null($grade->rawgrade) and is_null($grade->feedback)) {
1930             // no grade and feedback means no grading yet
1931             $grade->timemodified = null;
1933         } else if (!empty($dategraded)) {
1934             // fine - module sends info when graded (yay!)
1935             $grade->timemodified = $dategraded;
1937         } else if (grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)
1938                    or $grade->feedback !== $oldgrade->feedback) {
1939             // guess - if either grade or feedback changed set new graded date
1940             $grade->timemodified = time();
1942         } else {
1943             //keep original graded date
1944         }
1945         // end of hack alert
1947         $gradechanged = false;
1948         if (empty($grade->id)) {
1949             $result = (bool)$grade->insert($source);
1951             // If the grade insert was successful and the final grade was not null then trigger a user_graded event.
1952             if ($result && !is_null($grade->finalgrade)) {
1953                 \core\event\user_graded::create_from_grade($grade)->trigger();
1954             }
1955             $gradechanged = true;
1956         } else {
1957             // Existing grade_grades.
1959             if (grade_floats_different($grade->finalgrade,  $oldgrade->finalgrade)
1960                     or grade_floats_different($grade->rawgrade,    $oldgrade->rawgrade)
1961                     or grade_floats_different($grade->rawgrademin, $oldgrade->rawgrademin)
1962                     or grade_floats_different($grade->rawgrademax, $oldgrade->rawgrademax)
1963                     or $grade->rawscaleid != $oldgrade->rawscaleid) {
1964                 $gradechanged = true;
1965             }
1967             // The timecreated and timemodified checking is part of the hack above.
1968             if ($gradechanged === false and
1969                     $grade->feedback === $oldgrade->feedback and
1970                     $grade->feedbackformat == $oldgrade->feedbackformat and
1971                     $grade->timecreated == $oldgrade->timecreated and
1972                     $grade->timemodified == $oldgrade->timemodified) {
1973                 // No changes.
1974                 return $result;
1975             }
1976             $result = $grade->update($source);
1978             // If the grade update was successful and the actual grade has changed then trigger a user_graded event.
1979             if ($result && grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)) {
1980                 \core\event\user_graded::create_from_grade($grade)->trigger();
1981             }
1982         }
1984         if (!$result) {
1985             // Something went wrong - better force final grade recalculation.
1986             $this->force_regrading();
1987             return $result;
1988         }
1990         // If we are not updating grades we don't need to recalculate the whole course.
1991         if (!$gradechanged) {
1992             return $result;
1993         }
1995         if (!$this->needsupdate) {
1996             $course_item = grade_item::fetch_course_item($this->courseid);
1997             if (!$course_item->needsupdate) {
1998                 if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
1999                     $this->force_regrading();
2000                 }
2001             }
2002         }
2004         return $result;
2005     }
2007     /**
2008      * Calculates final grade values using the formula in the calculation property.
2009      * The parameters are taken from final grades of grade items in current course only.
2010      *
2011      * @param int $userid Supply a user ID to limit the calculations to the grades of a single user
2012      * @return bool false if error
2013      */
2014     public function compute($userid=null) {
2015         global $CFG, $DB;
2017         if (!$this->is_calculated()) {
2018             return false;
2019         }
2021         require_once($CFG->libdir.'/mathslib.php');
2023         if ($this->is_locked()) {
2024             return true; // no need to recalculate locked items
2025         }
2027         // Precreate grades - we need them to exist
2028         if ($userid) {
2029             $missing = array();
2030             if (!$DB->record_exists('grade_grades', array('itemid'=>$this->id, 'userid'=>$userid))) {
2031                 $m = new stdClass();
2032                 $m->userid = $userid;
2033                 $missing[] = $m;
2034             }
2035         } else {
2036             // Find any users who have grades for some but not all grade items in this course
2037             $params = array('gicourseid' => $this->courseid, 'ggitemid' => $this->id);
2038             $sql = "SELECT gg.userid
2039                       FROM {grade_grades} gg
2040                            JOIN {grade_items} gi
2041                            ON (gi.id = gg.itemid AND gi.courseid = :gicourseid)
2042                      GROUP BY gg.userid
2043                      HAVING SUM(CASE WHEN gg.itemid = :ggitemid THEN 1 ELSE 0 END) = 0";
2044             $missing = $DB->get_records_sql($sql, $params);
2045         }
2047         if ($missing) {
2048             foreach ($missing as $m) {
2049                 $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$m->userid), false);
2050                 $grade->grade_item =& $this;
2051                 $grade->insert('system');
2052             }
2053         }
2055         // get used items
2056         $useditems = $this->depends_on();
2058         // prepare formula and init maths library
2059         $formula = preg_replace('/##(gi\d+)##/', '\1', $this->calculation);
2060         if (strpos($formula, '[[') !== false) {
2061             // missing item
2062             return false;
2063         }
2064         $this->formula = new calc_formula($formula);
2066         // where to look for final grades?
2067         // this itemid is added so that we use only one query for source and final grades
2068         $gis = array_merge($useditems, array($this->id));
2069         list($usql, $params) = $DB->get_in_or_equal($gis);
2071         if ($userid) {
2072             $usersql = "AND g.userid=?";
2073             $params[] = $userid;
2074         } else {
2075             $usersql = "";
2076         }
2078         $grade_inst = new grade_grade();
2079         $fields = 'g.'.implode(',g.', $grade_inst->required_fields);
2081         $params[] = $this->courseid;
2082         $sql = "SELECT $fields
2083                   FROM {grade_grades} g, {grade_items} gi
2084                  WHERE gi.id = g.itemid AND gi.id $usql $usersql AND gi.courseid=?
2085                  ORDER BY g.userid";
2087         $return = true;
2089         // group the grades by userid and use formula on the group
2090         $rs = $DB->get_recordset_sql($sql, $params);
2091         if ($rs->valid()) {
2092             $prevuser = 0;
2093             $grade_records   = array();
2094             $oldgrade    = null;
2095             foreach ($rs as $used) {
2096                 if ($used->userid != $prevuser) {
2097                     if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {
2098                         $return = false;
2099                     }
2100                     $prevuser = $used->userid;
2101                     $grade_records   = array();
2102                     $oldgrade    = null;
2103                 }
2104                 if ($used->itemid == $this->id) {
2105                     $oldgrade = $used;
2106                 }
2107                 $grade_records['gi'.$used->itemid] = $used->finalgrade;
2108             }
2109             if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {
2110                 $return = false;
2111             }
2112         }
2113         $rs->close();
2115         return $return;
2116     }
2118     /**
2119      * Internal function that does the final grade calculation
2120      *
2121      * @param int $userid The user ID
2122      * @param array $params An array of grade items of the form {'gi'.$itemid]} => $finalgrade
2123      * @param array $useditems An array of grade item IDs that this grade item depends on plus its own ID
2124      * @param grade_grade $oldgrade A grade_grade instance containing the old values from the database
2125      * @return bool False if an error occurred
2126      */
2127     public function use_formula($userid, $params, $useditems, $oldgrade) {
2128         if (empty($userid)) {
2129             return true;
2130         }
2132         // add missing final grade values
2133         // not graded (null) is counted as 0 - the spreadsheet way
2134         $allinputsnull = true;
2135         foreach($useditems as $gi) {
2136             if (!array_key_exists('gi'.$gi, $params) || is_null($params['gi'.$gi])) {
2137                 $params['gi'.$gi] = 0;
2138             } else {
2139                 $params['gi'.$gi] = (float)$params['gi'.$gi];
2140                 if ($gi != $this->id) {
2141                     $allinputsnull = false;
2142                 }
2143             }
2144         }
2146         // can not use own final grade during calculation
2147         unset($params['gi'.$this->id]);
2149         // Check to see if the gradebook is frozen. This allows grades to not be altered at all until a user verifies that they
2150         // wish to update the grades.
2151         $gradebookcalculationsfreeze = get_config('core', 'gradebook_calculations_freeze_' . $this->courseid);
2153         $rawminandmaxchanged = false;
2154         // insert final grade - will be needed later anyway
2155         if ($oldgrade) {
2156             // Only run through this code if the gradebook isn't frozen.
2157             if ($gradebookcalculationsfreeze && (int)$gradebookcalculationsfreeze <= 20150627) {
2158                 // Do nothing.
2159             } else {
2160                 // The grade_grade for a calculated item should have the raw grade maximum and minimum set to the
2161                 // grade_item grade maximum and minimum respectively.
2162                 if ($oldgrade->rawgrademax != $this->grademax || $oldgrade->rawgrademin != $this->grademin) {
2163                     $rawminandmaxchanged = true;
2164                     $oldgrade->rawgrademax = $this->grademax;
2165                     $oldgrade->rawgrademin = $this->grademin;
2166                 }
2167             }
2168             $oldfinalgrade = $oldgrade->finalgrade;
2169             $grade = new grade_grade($oldgrade, false); // fetching from db is not needed
2170             $grade->grade_item =& $this;
2172         } else {
2173             $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid), false);
2174             $grade->grade_item =& $this;
2175             $rawminandmaxchanged = false;
2176             if ($gradebookcalculationsfreeze && (int)$gradebookcalculationsfreeze <= 20150627) {
2177                 // Do nothing.
2178             } else {
2179                 // The grade_grade for a calculated item should have the raw grade maximum and minimum set to the
2180                 // grade_item grade maximum and minimum respectively.
2181                 $rawminandmaxchanged = true;
2182                 $grade->rawgrademax = $this->grademax;
2183                 $grade->rawgrademin = $this->grademin;
2184             }
2185             $grade->insert('system');
2186             $oldfinalgrade = null;
2187         }
2189         // no need to recalculate locked or overridden grades
2190         if ($grade->is_locked() or $grade->is_overridden()) {
2191             return true;
2192         }
2194         if ($allinputsnull) {
2195             $grade->finalgrade = null;
2196             $result = true;
2198         } else {
2200             // do the calculation
2201             $this->formula->set_params($params);
2202             $result = $this->formula->evaluate();
2204             if ($result === false) {
2205                 $grade->finalgrade = null;
2207             } else {
2208                 // normalize
2209                 $grade->finalgrade = $this->bounded_grade($result);
2210             }
2211         }
2213         // Only run through this code if the gradebook isn't frozen.
2214         if ($gradebookcalculationsfreeze && (int)$gradebookcalculationsfreeze <= 20150627) {
2215             // Update in db if changed.
2216             if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) {
2217                 $grade->timemodified = time();
2218                 $success = $grade->update('compute');
2220                 // If successful trigger a user_graded event.
2221                 if ($success) {
2222                     \core\event\user_graded::create_from_grade($grade)->trigger();
2223                 }
2224             }
2225         } else {
2226             // Update in db if changed.
2227             if (grade_floats_different($grade->finalgrade, $oldfinalgrade) || $rawminandmaxchanged) {
2228                 $grade->timemodified = time();
2229                 $success = $grade->update('compute');
2231                 // If successful trigger a user_graded event.
2232                 if ($success) {
2233                     \core\event\user_graded::create_from_grade($grade)->trigger();
2234                 }
2235             }
2236         }
2238         if ($result !== false) {
2239             //lock grade if needed
2240         }
2242         if ($result === false) {
2243             return false;
2244         } else {
2245             return true;
2246         }
2248     }
2250     /**
2251      * Validate the formula.
2252      *
2253      * @param string $formulastr
2254      * @return bool true if calculation possible, false otherwise
2255      */
2256     public function validate_formula($formulastr) {
2257         global $CFG, $DB;
2258         require_once($CFG->libdir.'/mathslib.php');
2260         $formulastr = grade_item::normalize_formula($formulastr, $this->courseid);
2262         if (empty($formulastr)) {
2263             return true;
2264         }
2266         if (strpos($formulastr, '=') !== 0) {
2267             return get_string('errorcalculationnoequal', 'grades');
2268         }
2270         // get used items
2271         if (preg_match_all('/##gi(\d+)##/', $formulastr, $matches)) {
2272             $useditems = array_unique($matches[1]); // remove duplicates
2273         } else {
2274             $useditems = array();
2275         }
2277         // MDL-11902
2278         // unset the value if formula is trying to reference to itself
2279         // but array keys does not match itemid
2280         if (!empty($this->id)) {
2281             $useditems = array_diff($useditems, array($this->id));
2282             //unset($useditems[$this->id]);
2283         }
2285         // prepare formula and init maths library
2286         $formula = preg_replace('/##(gi\d+)##/', '\1', $formulastr);
2287         $formula = new calc_formula($formula);
2290         if (empty($useditems)) {
2291             $grade_items = array();
2293         } else {
2294             list($usql, $params) = $DB->get_in_or_equal($useditems);
2295             $params[] = $this->courseid;
2296             $sql = "SELECT gi.*
2297                       FROM {grade_items} gi
2298                      WHERE gi.id $usql and gi.courseid=?"; // from the same course only!
2300             if (!$grade_items = $DB->get_records_sql($sql, $params)) {
2301                 $grade_items = array();
2302             }
2303         }
2305         $params = array();
2306         foreach ($useditems as $itemid) {
2307             // make sure all grade items exist in this course
2308             if (!array_key_exists($itemid, $grade_items)) {
2309                 return false;
2310             }
2311             // use max grade when testing formula, this should be ok in 99.9%
2312             // division by 0 is one of possible problems
2313             $params['gi'.$grade_items[$itemid]->id] = $grade_items[$itemid]->grademax;
2314         }
2316         // do the calculation
2317         $formula->set_params($params);
2318         $result = $formula->evaluate();
2320         // false as result indicates some problem
2321         if ($result === false) {
2322             // TODO: add more error hints
2323             return get_string('errorcalculationunknown', 'grades');
2324         } else {
2325             return true;
2326         }
2327     }
2329     /**
2330      * Returns the value of the display type
2331      *
2332      * It can be set at 3 levels: grade_item, course setting and site. The lowest level overrides the higher ones.
2333      *
2334      * @return int Display type
2335      */
2336     public function get_displaytype() {
2337         global $CFG;
2339         if ($this->display == GRADE_DISPLAY_TYPE_DEFAULT) {
2340             return grade_get_setting($this->courseid, 'displaytype', $CFG->grade_displaytype);
2342         } else {
2343             return $this->display;
2344         }
2345     }
2347     /**
2348      * Returns the value of the decimals field
2349      *
2350      * It can be set at 3 levels: grade_item, course setting and site. The lowest level overrides the higher ones.
2351      *
2352      * @return int Decimals (0 - 5)
2353      */
2354     public function get_decimals() {
2355         global $CFG;
2357         if (is_null($this->decimals)) {
2358             return grade_get_setting($this->courseid, 'decimalpoints', $CFG->grade_decimalpoints);
2360         } else {
2361             return $this->decimals;
2362         }
2363     }
2365     /**
2366      * Returns a string representing the range of grademin - grademax for this grade item.
2367      *
2368      * @param int $rangesdisplaytype
2369      * @param int $rangesdecimalpoints
2370      * @return string
2371      */
2372     function get_formatted_range($rangesdisplaytype=null, $rangesdecimalpoints=null) {
2374         global $USER;
2376         // Determine which display type to use for this average
2377         if (isset($USER->gradeediting) && array_key_exists($this->courseid, $USER->gradeediting) && $USER->gradeediting[$this->courseid]) {
2378             $displaytype = GRADE_DISPLAY_TYPE_REAL;
2380         } else if ($rangesdisplaytype == GRADE_REPORT_PREFERENCE_INHERIT) { // no ==0 here, please resave report and user prefs
2381             $displaytype = $this->get_displaytype();
2383         } else {
2384             $displaytype = $rangesdisplaytype;
2385         }
2387         // Override grade_item setting if a display preference (not default) was set for the averages
2388         if ($rangesdecimalpoints == GRADE_REPORT_PREFERENCE_INHERIT) {
2389             $decimalpoints = $this->get_decimals();
2391         } else {
2392             $decimalpoints = $rangesdecimalpoints;
2393         }
2395         if ($displaytype == GRADE_DISPLAY_TYPE_PERCENTAGE) {
2396             $grademin = "0 %";
2397             $grademax = "100 %";
2399         } else {
2400             $grademin = grade_format_gradevalue($this->grademin, $this, true, $displaytype, $decimalpoints);
2401             $grademax = grade_format_gradevalue($this->grademax, $this, true, $displaytype, $decimalpoints);
2402         }
2404         return $grademin.'&ndash;'. $grademax;
2405     }
2407     /**
2408      * Queries parent categories recursively to find the aggregationcoef type that applies to this grade item.
2409      *
2410      * @return string|false Returns the coefficient string of false is no coefficient is being used
2411      */
2412     public function get_coefstring() {
2413         $parent_category = $this->load_parent_category();
2414         if ($this->is_category_item()) {
2415             $parent_category = $parent_category->load_parent_category();
2416         }
2418         if ($parent_category->is_aggregationcoef_used()) {
2419             return $parent_category->get_coefstring();
2420         } else {
2421             return false;
2422         }
2423     }
2425     /**
2426      * Returns whether the grade item can control the visibility of the grades
2427      *
2428      * @return bool
2429      */
2430     public function can_control_visibility() {
2431         if (core_component::get_plugin_directory($this->itemtype, $this->itemmodule)) {
2432             return !plugin_supports($this->itemtype, $this->itemmodule, FEATURE_CONTROLS_GRADE_VISIBILITY, false);
2433         }
2434         return parent::can_control_visibility();
2435     }
2437     /**
2438      * Used to notify the completion system (if necessary) that a user's grade
2439      * has changed, and clear up a possible score cache.
2440      *
2441      * @param bool $deleted True if grade was actually deleted
2442      */
2443     protected function notify_changed($deleted) {
2444         global $CFG;
2446         // Condition code may cache the grades for conditional availability of
2447         // modules or sections. (This code should use a hook for communication
2448         // with plugin, but hooks are not implemented at time of writing.)
2449         if (!empty($CFG->enableavailability) && class_exists('\availability_grade\callbacks')) {
2450             \availability_grade\callbacks::grade_item_changed($this->courseid);
2451         }
2452     }