Merge branch 'MDL-39740_grade_item_idnumber_fix' of git://github.com/ashleyholman...
[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                                  'sortorder', 'display', 'decimals', 'hidden', 'locked', 'locktime', 'needsupdate', 'timecreated',
54                                  '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
203      * @var float $aggregationcoef
204      */
205     public $aggregationcoef = 0;
207     /**
208      * Sorting order of the columns.
209      * @var int $sortorder
210      */
211     public $sortorder = 0;
213     /**
214      * Display type of the grades (Real, Percentage, Letter, or default).
215      * @var int $display
216      */
217     public $display = GRADE_DISPLAY_TYPE_DEFAULT;
219     /**
220      * The number of digits after the decimal point symbol. Applies only to REAL and PERCENTAGE grade display types.
221      * @var int $decimals
222      */
223     public $decimals = null;
225     /**
226      * Grade item lock flag. Empty if not locked, locked if any value present, usually date when item was locked. Locking prevents updating.
227      * @var int $locked
228      */
229     public $locked = 0;
231     /**
232      * Date after which the grade will be locked. Empty means no automatic locking.
233      * @var int $locktime
234      */
235     public $locktime = 0;
237     /**
238      * If set, the whole column will be recalculated, then this flag will be switched off.
239      * @var bool $needsupdate
240      */
241     public $needsupdate = 1;
243     /**
244      * Cached dependson array
245      * @var array An array of cached grade item dependencies.
246      */
247     public $dependson_cache = null;
249     /**
250      * In addition to update() as defined in grade_object, handle the grade_outcome and grade_scale objects.
251      * Force regrading if necessary, rounds the float numbers using php function,
252      * the reason is we need to compare the db value with computed number to skip regrading if possible.
253      *
254      * @param string $source from where was the object inserted (mod/forum, manual, etc.)
255      * @return bool success
256      */
257     public function update($source=null) {
258         // reset caches
259         $this->dependson_cache = null;
261         // Retrieve scale and infer grademax/min from it if needed
262         $this->load_scale();
264         // make sure there is not 0 in outcomeid
265         if (empty($this->outcomeid)) {
266             $this->outcomeid = null;
267         }
269         if ($this->qualifies_for_regrading()) {
270             $this->force_regrading();
271         }
273         $this->timemodified = time();
275         $this->grademin        = grade_floatval($this->grademin);
276         $this->grademax        = grade_floatval($this->grademax);
277         $this->multfactor      = grade_floatval($this->multfactor);
278         $this->plusfactor      = grade_floatval($this->plusfactor);
279         $this->aggregationcoef = grade_floatval($this->aggregationcoef);
281         return parent::update($source);
282     }
284     /**
285      * Compares the values held by this object with those of the matching record in DB, and returns
286      * whether or not these differences are sufficient to justify an update of all parent objects.
287      * This assumes that this object has an id number and a matching record in DB. If not, it will return false.
288      *
289      * @return bool
290      */
291     public function qualifies_for_regrading() {
292         if (empty($this->id)) {
293             return false;
294         }
296         $db_item = new grade_item(array('id' => $this->id));
298         $calculationdiff = $db_item->calculation != $this->calculation;
299         $categorydiff    = $db_item->categoryid  != $this->categoryid;
300         $gradetypediff   = $db_item->gradetype   != $this->gradetype;
301         $scaleiddiff     = $db_item->scaleid     != $this->scaleid;
302         $outcomeiddiff   = $db_item->outcomeid   != $this->outcomeid;
303         $locktimediff    = $db_item->locktime    != $this->locktime;
304         $grademindiff    = grade_floats_different($db_item->grademin,        $this->grademin);
305         $grademaxdiff    = grade_floats_different($db_item->grademax,        $this->grademax);
306         $multfactordiff  = grade_floats_different($db_item->multfactor,      $this->multfactor);
307         $plusfactordiff  = grade_floats_different($db_item->plusfactor,      $this->plusfactor);
308         $acoefdiff       = grade_floats_different($db_item->aggregationcoef, $this->aggregationcoef);
310         $needsupdatediff = !$db_item->needsupdate &&  $this->needsupdate;    // force regrading only if setting the flag first time
311         $lockeddiff      = !empty($db_item->locked) && empty($this->locked); // force regrading only when unlocking
313         return ($calculationdiff || $categorydiff || $gradetypediff || $grademaxdiff || $grademindiff || $scaleiddiff
314              || $outcomeiddiff || $multfactordiff || $plusfactordiff || $needsupdatediff
315              || $lockeddiff || $acoefdiff || $locktimediff);
316     }
318     /**
319      * Finds and returns a grade_item instance based on params.
320      *
321      * @static
322      * @param array $params associative arrays varname=>value
323      * @return grade_item|bool Returns a grade_item instance or false if none found
324      */
325     public static function fetch($params) {
326         return grade_object::fetch_helper('grade_items', 'grade_item', $params);
327     }
329     /**
330      * Finds and returns all grade_item instances based on params.
331      *
332      * @static
333      * @param array $params associative arrays varname=>value
334      * @return array array of grade_item instances or false if none found.
335      */
336     public static function fetch_all($params) {
337         return grade_object::fetch_all_helper('grade_items', 'grade_item', $params);
338     }
340     /**
341      * Delete all grades and force_regrading of parent category.
342      *
343      * @param string $source from where was the object deleted (mod/forum, manual, etc.)
344      * @return bool success
345      */
346     public function delete($source=null) {
347         $this->delete_all_grades($source);
348         return parent::delete($source);
349     }
351     /**
352      * Delete all grades
353      *
354      * @param string $source from where was the object deleted (mod/forum, manual, etc.)
355      * @return bool
356      */
357     public function delete_all_grades($source=null) {
358         if (!$this->is_course_item()) {
359             $this->force_regrading();
360         }
362         if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
363             foreach ($grades as $grade) {
364                 $grade->delete($source);
365             }
366         }
368         return true;
369     }
371     /**
372      * In addition to perform parent::insert(), calls force_regrading() method too.
373      *
374      * @param string $source From where was the object inserted (mod/forum, manual, etc.)
375      * @return int PK ID if successful, false otherwise
376      */
377     public function insert($source=null) {
378         global $CFG, $DB;
380         if (empty($this->courseid)) {
381             print_error('cannotinsertgrade');
382         }
384         // load scale if needed
385         $this->load_scale();
387         // add parent category if needed
388         if (empty($this->categoryid) and !$this->is_course_item() and !$this->is_category_item()) {
389             $course_category = grade_category::fetch_course_category($this->courseid);
390             $this->categoryid = $course_category->id;
392         }
394         // always place the new items at the end, move them after insert if needed
395         $last_sortorder = $DB->get_field_select('grade_items', 'MAX(sortorder)', "courseid = ?", array($this->courseid));
396         if (!empty($last_sortorder)) {
397             $this->sortorder = $last_sortorder + 1;
398         } else {
399             $this->sortorder = 1;
400         }
402         // add proper item numbers to manual items
403         if ($this->itemtype == 'manual') {
404             if (empty($this->itemnumber)) {
405                 $this->itemnumber = 0;
406             }
407         }
409         // make sure there is not 0 in outcomeid
410         if (empty($this->outcomeid)) {
411             $this->outcomeid = null;
412         }
414         $this->timecreated = $this->timemodified = time();
416         if (parent::insert($source)) {
417             // force regrading of items if needed
418             $this->force_regrading();
419             return $this->id;
421         } else {
422             debugging("Could not insert this grade_item in the database!");
423             return false;
424         }
425     }
427     /**
428      * Set idnumber of grade item, updates also course_modules table
429      *
430      * @param string $idnumber (without magic quotes)
431      * @return bool success
432      */
433     public function add_idnumber($idnumber) {
434         global $DB;
435         if (!empty($this->idnumber)) {
436             return false;
437         }
439         if ($this->itemtype == 'mod' and !$this->is_outcome_item()) {
440             if ($this->itemnumber == 0) {
441                 // for activity modules, itemnumber 0 is synced with the course_modules
442                 if (!$cm = get_coursemodule_from_instance($this->itemmodule, $this->iteminstance, $this->courseid)) {
443                     return false;
444                 }
445                 if (!empty($cm->idnumber)) {
446                     return false;
447                 }
448                 $DB->set_field('course_modules', 'idnumber', $idnumber, array('id' => $cm->id));
449                 $this->idnumber = $idnumber;
450                 return $this->update();
451             } else {
452                 $this->idnumber = $idnumber;
453                 return $this->update();
454             }
456         } else {
457             $this->idnumber = $idnumber;
458             return $this->update();
459         }
460     }
462     /**
463      * Returns the locked state of this grade_item (if the grade_item is locked OR no specific
464      * $userid is given) or the locked state of a specific grade within this item if a specific
465      * $userid is given and the grade_item is unlocked.
466      *
467      * @param int $userid The user's ID
468      * @return bool Locked state
469      */
470     public function is_locked($userid=NULL) {
471         if (!empty($this->locked)) {
472             return true;
473         }
475         if (!empty($userid)) {
476             if ($grade = grade_grade::fetch(array('itemid'=>$this->id, 'userid'=>$userid))) {
477                 $grade->grade_item =& $this; // prevent db fetching of cached grade_item
478                 return $grade->is_locked();
479             }
480         }
482         return false;
483     }
485     /**
486      * Locks or unlocks this grade_item and (optionally) all its associated final grades.
487      *
488      * @param int $lockedstate 0, 1 or a timestamp int(10) after which date the item will be locked.
489      * @param bool $cascade Lock/unlock child objects too
490      * @param bool $refresh Refresh grades when unlocking
491      * @return bool True if grade_item all grades updated, false if at least one update fails
492      */
493     public function set_locked($lockedstate, $cascade=false, $refresh=true) {
494         if ($lockedstate) {
495         /// setting lock
496             if ($this->needsupdate) {
497                 return false; // can not lock grade without first having final grade
498             }
500             $this->locked = time();
501             $this->update();
503             if ($cascade) {
504                 $grades = $this->get_final();
505                 foreach($grades as $g) {
506                     $grade = new grade_grade($g, false);
507                     $grade->grade_item =& $this;
508                     $grade->set_locked(1, null, false);
509                 }
510             }
512             return true;
514         } else {
515         /// removing lock
516             if (!empty($this->locked) and $this->locktime < time()) {
517                 //we have to reset locktime or else it would lock up again
518                 $this->locktime = 0;
519             }
521             $this->locked = 0;
522             $this->update();
524             if ($cascade) {
525                 if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
526                     foreach($grades as $grade) {
527                         $grade->grade_item =& $this;
528                         $grade->set_locked(0, null, false);
529                     }
530                 }
531             }
533             if ($refresh) {
534                 //refresh when unlocking
535                 $this->refresh_grades();
536             }
538             return true;
539         }
540     }
542     /**
543      * Lock the grade if needed. Make sure this is called only when final grades are valid
544      */
545     public function check_locktime() {
546         if (!empty($this->locked)) {
547             return; // already locked
548         }
550         if ($this->locktime and $this->locktime < time()) {
551             $this->locked = time();
552             $this->update('locktime');
553         }
554     }
556     /**
557      * Set the locktime for this grade item.
558      *
559      * @param int $locktime timestamp for lock to activate
560      * @return void
561      */
562     public function set_locktime($locktime) {
563         $this->locktime = $locktime;
564         $this->update();
565     }
567     /**
568      * Set the locktime for this grade item.
569      *
570      * @return int $locktime timestamp for lock to activate
571      */
572     public function get_locktime() {
573         return $this->locktime;
574     }
576     /**
577      * Set the hidden status of grade_item and all grades.
578      *
579      * 0 mean always visible, 1 means always hidden and a number > 1 is a timestamp to hide until
580      *
581      * @param int $hidden new hidden status
582      * @param bool $cascade apply to child objects too
583      */
584     public function set_hidden($hidden, $cascade=false) {
585         parent::set_hidden($hidden, $cascade);
587         if ($cascade) {
588             if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
589                 foreach($grades as $grade) {
590                     $grade->grade_item =& $this;
591                     $grade->set_hidden($hidden, $cascade);
592                 }
593             }
594         }
596         //if marking item visible make sure category is visible MDL-21367
597         if( !$hidden ) {
598             $category_array = grade_category::fetch_all(array('id'=>$this->categoryid));
599             if ($category_array && array_key_exists($this->categoryid, $category_array)) {
600                 $category = $category_array[$this->categoryid];
601                 //call set_hidden on the category regardless of whether it is hidden as its parent might be hidden
602                 //if($category->is_hidden()) {
603                     $category->set_hidden($hidden, false);
604                 //}
605             }
606         }
607     }
609     /**
610      * Returns the number of grades that are hidden
611      *
612      * @param string $groupsql SQL to limit the query by group
613      * @param array $params SQL params for $groupsql
614      * @param string $groupwheresql Where conditions for $groupsql
615      * @return int The number of hidden grades
616      */
617     public function has_hidden_grades($groupsql="", array $params=null, $groupwheresql="") {
618         global $DB;
619         $params = (array)$params;
620         $params['itemid'] = $this->id;
622         return $DB->get_field_sql("SELECT COUNT(*) FROM {grade_grades} g LEFT JOIN "
623                             ."{user} u ON g.userid = u.id $groupsql WHERE itemid = :itemid AND hidden = 1 $groupwheresql", $params);
624     }
626     /**
627      * Mark regrading as finished successfully.
628      */
629     public function regrading_finished() {
630         global $DB;
631         $this->needsupdate = 0;
632         //do not use $this->update() because we do not want this logged in grade_item_history
633         $DB->set_field('grade_items', 'needsupdate', 0, array('id' => $this->id));
634     }
636     /**
637      * Performs the necessary calculations on the grades_final referenced by this grade_item.
638      * Also resets the needsupdate flag once successfully performed.
639      *
640      * This function must be used ONLY from lib/gradeslib.php/grade_regrade_final_grades(),
641      * because the regrading must be done in correct order!!
642      *
643      * @param int $userid Supply a user ID to limit the regrading to a single user
644      * @return bool true if ok, error string otherwise
645      */
646     public function regrade_final_grades($userid=null) {
647         global $CFG, $DB;
649         // locked grade items already have correct final grades
650         if ($this->is_locked()) {
651             return true;
652         }
654         // calculation produces final value using formula from other final values
655         if ($this->is_calculated()) {
656             if ($this->compute($userid)) {
657                 return true;
658             } else {
659                 return "Could not calculate grades for grade item"; // TODO: improve and localize
660             }
662         // noncalculated outcomes already have final values - raw grades not used
663         } else if ($this->is_outcome_item()) {
664             return true;
666         // aggregate the category grade
667         } else if ($this->is_category_item() or $this->is_course_item()) {
668             // aggregate category grade item
669             $category = $this->get_item_category();
670             $category->grade_item =& $this;
671             if ($category->generate_grades($userid)) {
672                 return true;
673             } else {
674                 return "Could not aggregate final grades for category:".$this->id; // TODO: improve and localize
675             }
677         } else if ($this->is_manual_item()) {
678             // manual items track only final grades, no raw grades
679             return true;
681         } else if (!$this->is_raw_used()) {
682             // hmm - raw grades are not used- nothing to regrade
683             return true;
684         }
686         // normal grade item - just new final grades
687         $result = true;
688         $grade_inst = new grade_grade();
689         $fields = implode(',', $grade_inst->required_fields);
690         if ($userid) {
691             $params = array($this->id, $userid);
692             $rs = $DB->get_recordset_select('grade_grades', "itemid=? AND userid=?", $params, '', $fields);
693         } else {
694             $rs = $DB->get_recordset('grade_grades', array('itemid' => $this->id), '', $fields);
695         }
696         if ($rs) {
697             foreach ($rs as $grade_record) {
698                 $grade = new grade_grade($grade_record, false);
700                 if (!empty($grade_record->locked) or !empty($grade_record->overridden)) {
701                     // this grade is locked - final grade must be ok
702                     continue;
703                 }
705                 $grade->finalgrade = $this->adjust_raw_grade($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
707                 if (grade_floats_different($grade_record->finalgrade, $grade->finalgrade)) {
708                     if (!$grade->update('system')) {
709                         $result = "Internal error updating final grade";
710                     }
711                 }
712             }
713             $rs->close();
714         }
716         return $result;
717     }
719     /**
720      * Given a float grade value or integer grade scale, applies a number of adjustment based on
721      * grade_item variables and returns the result.
722      *
723      * @param float $rawgrade The raw grade value
724      * @param float $rawmin original rawmin
725      * @param float $rawmax original rawmax
726      * @return mixed
727      */
728     public function adjust_raw_grade($rawgrade, $rawmin, $rawmax) {
729         if (is_null($rawgrade)) {
730             return null;
731         }
733         if ($this->gradetype == GRADE_TYPE_VALUE) { // Dealing with numerical grade
735             if ($this->grademax < $this->grademin) {
736                 return null;
737             }
739             if ($this->grademax == $this->grademin) {
740                 return $this->grademax; // no range
741             }
743             // Standardise score to the new grade range
744             // NOTE: this is not compatible with current assignment grading
745             $isassignmentmodule = ($this->itemmodule == 'assignment') || ($this->itemmodule == 'assign');
746             if (!$isassignmentmodule && ($rawmin != $this->grademin or $rawmax != $this->grademax)) {
747                 $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax);
748             }
750             // Apply other grade_item factors
751             $rawgrade *= $this->multfactor;
752             $rawgrade += $this->plusfactor;
754             return $this->bounded_grade($rawgrade);
756         } else if ($this->gradetype == GRADE_TYPE_SCALE) { // Dealing with a scale value
757             if (empty($this->scale)) {
758                 $this->load_scale();
759             }
761             if ($this->grademax < 0) {
762                 return null; // scale not present - no grade
763             }
765             if ($this->grademax == 0) {
766                 return $this->grademax; // only one option
767             }
769             // Convert scale if needed
770             // NOTE: this is not compatible with current assignment grading
771             if ($this->itemmodule != 'assignment' and ($rawmin != $this->grademin or $rawmax != $this->grademax)) {
772                 $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax);
773             }
775             return $this->bounded_grade($rawgrade);
778         } else if ($this->gradetype == GRADE_TYPE_TEXT or $this->gradetype == GRADE_TYPE_NONE) { // no value
779             // somebody changed the grading type when grades already existed
780             return null;
782         } else {
783             debugging("Unknown grade type");
784             return null;
785         }
786     }
788     /**
789      * Sets this grade_item's needsupdate to true. Also marks the course item as needing update.
790      *
791      * @return void
792      */
793     public function force_regrading() {
794         global $DB;
795         $this->needsupdate = 1;
796         //mark this item and course item only - categories and calculated items are always regraded
797         $wheresql = "(itemtype='course' OR id=?) AND courseid=?";
798         $params   = array($this->id, $this->courseid);
799         $DB->set_field_select('grade_items', 'needsupdate', 1, $wheresql, $params);
800     }
802     /**
803      * Instantiates a grade_scale object from the DB if this item's scaleid variable is set
804      *
805      * @return grade_scale Returns a grade_scale object or null if no scale used
806      */
807     public function load_scale() {
808         if ($this->gradetype != GRADE_TYPE_SCALE) {
809             $this->scaleid = null;
810         }
812         if (!empty($this->scaleid)) {
813             //do not load scale if already present
814             if (empty($this->scale->id) or $this->scale->id != $this->scaleid) {
815                 $this->scale = grade_scale::fetch(array('id'=>$this->scaleid));
816                 if (!$this->scale) {
817                     debugging('Incorrect scale id: '.$this->scaleid);
818                     $this->scale = null;
819                     return null;
820                 }
821                 $this->scale->load_items();
822             }
824             // Until scales are uniformly set to min=0 max=count(scaleitems)-1 throughout Moodle, we
825             // stay with the current min=1 max=count(scaleitems)
826             $this->grademax = count($this->scale->scale_items);
827             $this->grademin = 1;
829         } else {
830             $this->scale = null;
831         }
833         return $this->scale;
834     }
836     /**
837      * Instantiates a grade_outcome object from the DB if this item's outcomeid variable is set
838      *
839      * @return grade_outcome This grade item's associated grade_outcome or null
840      */
841     public function load_outcome() {
842         if (!empty($this->outcomeid)) {
843             $this->outcome = grade_outcome::fetch(array('id'=>$this->outcomeid));
844         }
845         return $this->outcome;
846     }
848     /**
849      * Returns the grade_category object this grade_item belongs to (referenced by categoryid)
850      * or category attached to category item.
851      *
852      * @return grade_category|bool Returns a grade_category object if applicable or false if this is a course item
853      */
854     public function get_parent_category() {
855         if ($this->is_category_item() or $this->is_course_item()) {
856             return $this->get_item_category();
858         } else {
859             return grade_category::fetch(array('id'=>$this->categoryid));
860         }
861     }
863     /**
864      * Calls upon the get_parent_category method to retrieve the grade_category object
865      * from the DB and assigns it to $this->parent_category. It also returns the object.
866      *
867      * @return grade_category This grade item's parent grade_category.
868      */
869     public function load_parent_category() {
870         if (empty($this->parent_category->id)) {
871             $this->parent_category = $this->get_parent_category();
872         }
873         return $this->parent_category;
874     }
876     /**
877      * Returns the grade_category for a grade category grade item
878      *
879      * @return grade_category|bool Returns a grade_category instance if applicable or false otherwise
880      */
881     public function get_item_category() {
882         if (!$this->is_course_item() and !$this->is_category_item()) {
883             return false;
884         }
885         return grade_category::fetch(array('id'=>$this->iteminstance));
886     }
888     /**
889      * Calls upon the get_item_category method to retrieve the grade_category object
890      * from the DB and assigns it to $this->item_category. It also returns the object.
891      *
892      * @return grade_category
893      */
894     public function load_item_category() {
895         if (empty($this->item_category->id)) {
896             $this->item_category = $this->get_item_category();
897         }
898         return $this->item_category;
899     }
901     /**
902      * Is the grade item associated with category?
903      *
904      * @return bool
905      */
906     public function is_category_item() {
907         return ($this->itemtype == 'category');
908     }
910     /**
911      * Is the grade item associated with course?
912      *
913      * @return bool
914      */
915     public function is_course_item() {
916         return ($this->itemtype == 'course');
917     }
919     /**
920      * Is this a manually graded item?
921      *
922      * @return bool
923      */
924     public function is_manual_item() {
925         return ($this->itemtype == 'manual');
926     }
928     /**
929      * Is this an outcome item?
930      *
931      * @return bool
932      */
933     public function is_outcome_item() {
934         return !empty($this->outcomeid);
935     }
937     /**
938      * Is the grade item external - associated with module, plugin or something else?
939      *
940      * @return bool
941      */
942     public function is_external_item() {
943         return ($this->itemtype == 'mod');
944     }
946     /**
947      * Is the grade item overridable
948      *
949      * @return bool
950      */
951     public function is_overridable_item() {
952         return !$this->is_outcome_item() and ($this->is_external_item() or $this->is_calculated() or $this->is_course_item() or $this->is_category_item());
953     }
955     /**
956      * Is the grade item feedback overridable
957      *
958      * @return bool
959      */
960     public function is_overridable_item_feedback() {
961         return !$this->is_outcome_item() and $this->is_external_item();
962     }
964     /**
965      * Returns true if grade items uses raw grades
966      *
967      * @return bool
968      */
969     public function is_raw_used() {
970         return ($this->is_external_item() and !$this->is_calculated() and !$this->is_outcome_item());
971     }
973     /**
974      * Returns the grade item associated with the course
975      *
976      * @param int $courseid
977      * @return grade_item Course level grade item object
978      */
979     public static function fetch_course_item($courseid) {
980         if ($course_item = grade_item::fetch(array('courseid'=>$courseid, 'itemtype'=>'course'))) {
981             return $course_item;
982         }
984         // first get category - it creates the associated grade item
985         $course_category = grade_category::fetch_course_category($courseid);
986         return $course_category->get_grade_item();
987     }
989     /**
990      * Is grading object editable?
991      *
992      * @return bool
993      */
994     public function is_editable() {
995         return true;
996     }
998     /**
999      * Checks if grade calculated. Returns this object's calculation.
1000      *
1001      * @return bool true if grade item calculated.
1002      */
1003     public function is_calculated() {
1004         if (empty($this->calculation)) {
1005             return false;
1006         }
1008         /*
1009          * The main reason why we use the ##gixxx## instead of [[idnumber]] is speed of depends_on(),
1010          * we would have to fetch all course grade items to find out the ids.
1011          * Also if user changes the idnumber the formula does not need to be updated.
1012          */
1014         // first detect if we need to change calculation formula from [[idnumber]] to ##giXXX## (after backup, etc.)
1015         if (!$this->calculation_normalized and strpos($this->calculation, '[[') !== false) {
1016             $this->set_calculation($this->calculation);
1017         }
1019         return !empty($this->calculation);
1020     }
1022     /**
1023      * Returns calculation string if grade calculated.
1024      *
1025      * @return string Returns the grade item's calculation if calculation is used, null if not
1026      */
1027     public function get_calculation() {
1028         if ($this->is_calculated()) {
1029             return grade_item::denormalize_formula($this->calculation, $this->courseid);
1031         } else {
1032             return NULL;
1033         }
1034     }
1036     /**
1037      * Sets this item's calculation (creates it) if not yet set, or
1038      * updates it if already set (in the DB). If no calculation is given,
1039      * the calculation is removed.
1040      *
1041      * @param string $formula string representation of formula used for calculation
1042      * @return bool success
1043      */
1044     public function set_calculation($formula) {
1045         $this->calculation = grade_item::normalize_formula($formula, $this->courseid);
1046         $this->calculation_normalized = true;
1047         return $this->update();
1048     }
1050     /**
1051      * Denormalizes the calculation formula to [idnumber] form
1052      *
1053      * @param string $formula A string representation of the formula
1054      * @param int $courseid The course ID
1055      * @return string The denormalized formula as a string
1056      */
1057     public static function denormalize_formula($formula, $courseid) {
1058         if (empty($formula)) {
1059             return '';
1060         }
1062         // denormalize formula - convert ##giXX## to [[idnumber]]
1063         if (preg_match_all('/##gi(\d+)##/', $formula, $matches)) {
1064             foreach ($matches[1] as $id) {
1065                 if ($grade_item = grade_item::fetch(array('id'=>$id, 'courseid'=>$courseid))) {
1066                     if (!empty($grade_item->idnumber)) {
1067                         $formula = str_replace('##gi'.$grade_item->id.'##', '[['.$grade_item->idnumber.']]', $formula);
1068                     }
1069                 }
1070             }
1071         }
1073         return $formula;
1075     }
1077     /**
1078      * Normalizes the calculation formula to [#giXX#] form
1079      *
1080      * @param string $formula The formula
1081      * @param int $courseid The course ID
1082      * @return string The normalized formula as a string
1083      */
1084     public static function normalize_formula($formula, $courseid) {
1085         $formula = trim($formula);
1087         if (empty($formula)) {
1088             return NULL;
1090         }
1092         // normalize formula - we want grade item ids ##giXXX## instead of [[idnumber]]
1093         if ($grade_items = grade_item::fetch_all(array('courseid'=>$courseid))) {
1094             foreach ($grade_items as $grade_item) {
1095                 $formula = str_replace('[['.$grade_item->idnumber.']]', '##gi'.$grade_item->id.'##', $formula);
1096             }
1097         }
1099         return $formula;
1100     }
1102     /**
1103      * Returns the final values for this grade item (as imported by module or other source).
1104      *
1105      * @param int $userid Optional: to retrieve a single user's final grade
1106      * @return array|grade_grade An array of all grade_grade instances for this grade_item, or a single grade_grade instance.
1107      */
1108     public function get_final($userid=NULL) {
1109         global $DB;
1110         if ($userid) {
1111             if ($user = $DB->get_record('grade_grades', array('itemid' => $this->id, 'userid' => $userid))) {
1112                 return $user;
1113             }
1115         } else {
1116             if ($grades = $DB->get_records('grade_grades', array('itemid' => $this->id))) {
1117                 //TODO: speed up with better SQL (MDL-31380)
1118                 $result = array();
1119                 foreach ($grades as $grade) {
1120                     $result[$grade->userid] = $grade;
1121                 }
1122                 return $result;
1123             } else {
1124                 return array();
1125             }
1126         }
1127     }
1129     /**
1130      * Get (or create if not exist yet) grade for this user
1131      *
1132      * @param int $userid The user ID
1133      * @param bool $create If true and the user has no grade for this grade item a new grade_grade instance will be inserted
1134      * @return grade_grade The grade_grade instance for the user for this grade item
1135      */
1136     public function get_grade($userid, $create=true) {
1137         if (empty($this->id)) {
1138             debugging('Can not use before insert');
1139             return false;
1140         }
1142         $grade = new grade_grade(array('userid'=>$userid, 'itemid'=>$this->id));
1143         if (empty($grade->id) and $create) {
1144             $grade->insert();
1145         }
1147         return $grade;
1148     }
1150     /**
1151      * Returns the sortorder of this grade_item. This method is also available in
1152      * grade_category, for cases where the object type is not know.
1153      *
1154      * @return int Sort order
1155      */
1156     public function get_sortorder() {
1157         return $this->sortorder;
1158     }
1160     /**
1161      * Returns the idnumber of this grade_item. This method is also available in
1162      * grade_category, for cases where the object type is not know.
1163      *
1164      * @return string The grade item idnumber
1165      */
1166     public function get_idnumber() {
1167         return $this->idnumber;
1168     }
1170     /**
1171      * Returns this grade_item. This method is also available in
1172      * grade_category, for cases where the object type is not know.
1173      *
1174      * @return grade_item
1175      */
1176     public function get_grade_item() {
1177         return $this;
1178     }
1180     /**
1181      * Sets the sortorder of this grade_item. This method is also available in
1182      * grade_category, for cases where the object type is not know.
1183      *
1184      * @param int $sortorder
1185      */
1186     public function set_sortorder($sortorder) {
1187         if ($this->sortorder == $sortorder) {
1188             return;
1189         }
1190         $this->sortorder = $sortorder;
1191         $this->update();
1192     }
1194     /**
1195      * Update this grade item's sortorder so that it will appear after $sortorder
1196      *
1197      * @param int $sortorder The sort order to place this grade item after
1198      */
1199     public function move_after_sortorder($sortorder) {
1200         global $CFG, $DB;
1202         //make some room first
1203         $params = array($sortorder, $this->courseid);
1204         $sql = "UPDATE {grade_items}
1205                    SET sortorder = sortorder + 1
1206                  WHERE sortorder > ? AND courseid = ?";
1207         $DB->execute($sql, $params);
1209         $this->set_sortorder($sortorder + 1);
1210     }
1212     /**
1213      * Detect duplicate grade item's sortorder and re-sort them.
1214      * Note: Duplicate sortorder will be introduced while duplicating activities or
1215      * merging two courses.
1216      *
1217      * @param int $courseid id of the course for which grade_items sortorder need to be fixed.
1218      */
1219     public static function fix_duplicate_sortorder($courseid) {
1220         global $DB;
1222         $transaction = $DB->start_delegated_transaction();
1224         $sql = "SELECT DISTINCT g1.id, g1.courseid, g1.sortorder
1225                     FROM {grade_items} g1
1226                     JOIN {grade_items} g2 ON g1.courseid = g2.courseid
1227                 WHERE g1.sortorder = g2.sortorder AND g1.id != g2.id AND g1.courseid = :courseid
1228                 ORDER BY g1.sortorder DESC, g1.id DESC";
1230         // Get all duplicates in course highest sort order, and higest id first so that we can make space at the
1231         // bottom higher end of the sort orders and work down by id.
1232         $rs = $DB->get_recordset_sql($sql, array('courseid' => $courseid));
1234         foreach($rs as $duplicate) {
1235             $DB->execute("UPDATE {grade_items}
1236                             SET sortorder = sortorder + 1
1237                           WHERE courseid = :courseid AND
1238                           (sortorder > :sortorder OR (sortorder = :sortorder2 AND id > :id))",
1239                 array('courseid' => $duplicate->courseid,
1240                     'sortorder' => $duplicate->sortorder,
1241                     'sortorder2' => $duplicate->sortorder,
1242                     'id' => $duplicate->id));
1243         }
1244         $rs->close();
1245         $transaction->allow_commit();
1246     }
1248     /**
1249      * Returns the most descriptive field for this object.
1250      *
1251      * Determines what type of grade item it is then returns the appropriate string
1252      *
1253      * @param bool $fulltotal If the item is a category total, returns $categoryname."total" instead of "Category total" or "Course total"
1254      * @return string name
1255      */
1256     public function get_name($fulltotal=false) {
1257         if (!empty($this->itemname)) {
1258             // MDL-10557
1259             return format_string($this->itemname);
1261         } else if ($this->is_course_item()) {
1262             return get_string('coursetotal', 'grades');
1264         } else if ($this->is_category_item()) {
1265             if ($fulltotal) {
1266                 $category = $this->load_parent_category();
1267                 $a = new stdClass();
1268                 $a->category = $category->get_name();
1269                 return get_string('categorytotalfull', 'grades', $a);
1270             } else {
1271             return get_string('categorytotal', 'grades');
1272             }
1274         } else {
1275             return get_string('grade');
1276         }
1277     }
1279     /**
1280      * Sets this item's categoryid. A generic method shared by objects that have a parent id of some kind.
1281      *
1282      * @param int $parentid The ID of the new parent
1283      * @return bool True if success
1284      */
1285     public function set_parent($parentid) {
1286         if ($this->is_course_item() or $this->is_category_item()) {
1287             print_error('cannotsetparentforcatoritem');
1288         }
1290         if ($this->categoryid == $parentid) {
1291             return true;
1292         }
1294         // find parent and check course id
1295         if (!$parent_category = grade_category::fetch(array('id'=>$parentid, 'courseid'=>$this->courseid))) {
1296             return false;
1297         }
1299         // MDL-19407 If moving from a non-SWM category to a SWM category, convert aggregationcoef to 0
1300         $currentparent = $this->load_parent_category();
1302         if ($currentparent->aggregation != GRADE_AGGREGATE_WEIGHTED_MEAN2 && $parent_category->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN2) {
1303             $this->aggregationcoef = 0;
1304         }
1306         $this->force_regrading();
1308         // set new parent
1309         $this->categoryid = $parent_category->id;
1310         $this->parent_category =& $parent_category;
1312         return $this->update();
1313     }
1315     /**
1316      * Makes sure value is a valid grade value.
1317      *
1318      * @param float $gradevalue
1319      * @return mixed float or int fixed grade value
1320      */
1321     public function bounded_grade($gradevalue) {
1322         global $CFG;
1324         if (is_null($gradevalue)) {
1325             return null;
1326         }
1328         if ($this->gradetype == GRADE_TYPE_SCALE) {
1329             // no >100% grades hack for scale grades!
1330             // 1.5 is rounded to 2 ;-)
1331             return (int)bounded_number($this->grademin, round($gradevalue+0.00001), $this->grademax);
1332         }
1334         $grademax = $this->grademax;
1336         // NOTE: if you change this value you must manually reset the needsupdate flag in all grade items
1337         $maxcoef = isset($CFG->gradeoverhundredprocentmax) ? $CFG->gradeoverhundredprocentmax : 10; // 1000% max by default
1339         if (!empty($CFG->unlimitedgrades)) {
1340             // NOTE: if you change this value you must manually reset the needsupdate flag in all grade items
1341             $grademax = $grademax * $maxcoef;
1342         } else if ($this->is_category_item() or $this->is_course_item()) {
1343             $category = $this->load_item_category();
1344             if ($category->aggregation >= 100) {
1345                 // grade >100% hack
1346                 $grademax = $grademax * $maxcoef;
1347             }
1348         }
1350         return (float)bounded_number($this->grademin, $gradevalue, $grademax);
1351     }
1353     /**
1354      * Finds out on which other items does this depend directly when doing calculation or category aggregation
1355      *
1356      * @param bool $reset_cache
1357      * @return array of grade_item IDs this one depends on
1358      */
1359     public function depends_on($reset_cache=false) {
1360         global $CFG, $DB;
1362         if ($reset_cache) {
1363             $this->dependson_cache = null;
1364         } else if (isset($this->dependson_cache)) {
1365             return $this->dependson_cache;
1366         }
1368         if ($this->is_locked()) {
1369             // locked items do not need to be regraded
1370             $this->dependson_cache = array();
1371             return $this->dependson_cache;
1372         }
1374         if ($this->is_calculated()) {
1375             if (preg_match_all('/##gi(\d+)##/', $this->calculation, $matches)) {
1376                 $this->dependson_cache = array_unique($matches[1]); // remove duplicates
1377                 return $this->dependson_cache;
1378             } else {
1379                 $this->dependson_cache = array();
1380                 return $this->dependson_cache;
1381             }
1383         } else if ($grade_category = $this->load_item_category()) {
1384             $params = array();
1386             //only items with numeric or scale values can be aggregated
1387             if ($this->gradetype != GRADE_TYPE_VALUE and $this->gradetype != GRADE_TYPE_SCALE) {
1388                 $this->dependson_cache = array();
1389                 return $this->dependson_cache;
1390             }
1392             $grade_category->apply_forced_settings();
1394             if (empty($CFG->enableoutcomes) or $grade_category->aggregateoutcomes) {
1395                 $outcomes_sql = "";
1396             } else {
1397                 $outcomes_sql = "AND gi.outcomeid IS NULL";
1398             }
1400             if (empty($CFG->grade_includescalesinaggregation)) {
1401                 $gtypes = "gi.gradetype = ?";
1402                 $params[] = GRADE_TYPE_VALUE;
1403             } else {
1404                 $gtypes = "(gi.gradetype = ? OR gi.gradetype = ?)";
1405                 $params[] = GRADE_TYPE_VALUE;
1406                 $params[] = GRADE_TYPE_SCALE;
1407             }
1409             if ($grade_category->aggregatesubcats) {
1410                 // return all children excluding category items
1411                 $params[] = $this->courseid;
1412                 $params[] = '%/' . $grade_category->id . '/%';
1413                 $sql = "SELECT gi.id
1414                           FROM {grade_items} gi
1415                           JOIN {grade_categories} gc ON gi.categoryid = gc.id
1416                          WHERE $gtypes
1417                                $outcomes_sql
1418                                AND gi.courseid = ?
1419                                AND gc.path LIKE ?";
1420             } else {
1421                 $params[] = $grade_category->id;
1422                 $params[] = $this->courseid;
1423                 $params[] = $grade_category->id;
1424                 $params[] = $this->courseid;
1425                 if (empty($CFG->grade_includescalesinaggregation)) {
1426                     $params[] = GRADE_TYPE_VALUE;
1427                 } else {
1428                     $params[] = GRADE_TYPE_VALUE;
1429                     $params[] = GRADE_TYPE_SCALE;
1430                 }
1431                 $sql = "SELECT gi.id
1432                           FROM {grade_items} gi
1433                          WHERE $gtypes
1434                                AND gi.categoryid = ?
1435                                AND gi.courseid = ?
1436                                $outcomes_sql
1437                         UNION
1439                         SELECT gi.id
1440                           FROM {grade_items} gi, {grade_categories} gc
1441                          WHERE (gi.itemtype = 'category' OR gi.itemtype = 'course') AND gi.iteminstance=gc.id
1442                                AND gc.parent = ?
1443                                AND gi.courseid = ?
1444                                AND $gtypes
1445                                $outcomes_sql";
1446             }
1448             if ($children = $DB->get_records_sql($sql, $params)) {
1449                 $this->dependson_cache = array_keys($children);
1450                 return $this->dependson_cache;
1451             } else {
1452                 $this->dependson_cache = array();
1453                 return $this->dependson_cache;
1454             }
1456         } else {
1457             $this->dependson_cache = array();
1458             return $this->dependson_cache;
1459         }
1460     }
1462     /**
1463      * Refetch grades from modules, plugins.
1464      *
1465      * @param int $userid optional, limit the refetch to a single user
1466      * @return bool Returns true on success or if there is nothing to do
1467      */
1468     public function refresh_grades($userid=0) {
1469         global $DB;
1470         if ($this->itemtype == 'mod') {
1471             if ($this->is_outcome_item()) {
1472                 //nothing to do
1473                 return true;
1474             }
1476             if (!$activity = $DB->get_record($this->itemmodule, array('id' => $this->iteminstance))) {
1477                 debugging("Can not find $this->itemmodule activity with id $this->iteminstance");
1478                 return false;
1479             }
1481             if (!$cm = get_coursemodule_from_instance($this->itemmodule, $activity->id, $this->courseid)) {
1482                 debugging('Can not find course module');
1483                 return false;
1484             }
1486             $activity->modname    = $this->itemmodule;
1487             $activity->cmidnumber = $cm->idnumber;
1489             return grade_update_mod_grades($activity, $userid);
1490         }
1492         return true;
1493     }
1495     /**
1496      * Updates final grade value for given user, this is a only way to update final
1497      * grades from gradebook and import because it logs the change in history table
1498      * and deals with overridden flag. This flag is set to prevent later overriding
1499      * from raw grades submitted from modules.
1500      *
1501      * @param int $userid The graded user
1502      * @param float|false $finalgrade The float value of final grade, false means do not change
1503      * @param string $source The modification source
1504      * @param string $feedback Optional teacher feedback
1505      * @param int $feedbackformat A format like FORMAT_PLAIN or FORMAT_HTML
1506      * @param int $usermodified The ID of the user making the modification
1507      * @return bool success
1508      */
1509     public function update_final_grade($userid, $finalgrade=false, $source=NULL, $feedback=false, $feedbackformat=FORMAT_MOODLE, $usermodified=null) {
1510         global $USER, $CFG;
1512         $result = true;
1514         // no grading used or locked
1515         if ($this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) {
1516             return false;
1517         }
1519         $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid));
1520         $grade->grade_item =& $this; // prevent db fetching of this grade_item
1522         if (empty($usermodified)) {
1523             $grade->usermodified = $USER->id;
1524         } else {
1525             $grade->usermodified = $usermodified;
1526         }
1528         if ($grade->is_locked()) {
1529             // do not update locked grades at all
1530             return false;
1531         }
1533         $locktime = $grade->get_locktime();
1534         if ($locktime and $locktime < time()) {
1535             // do not update grades that should be already locked, force regrade instead
1536             $this->force_regrading();
1537             return false;
1538         }
1540         $oldgrade = new stdClass();
1541         $oldgrade->finalgrade     = $grade->finalgrade;
1542         $oldgrade->overridden     = $grade->overridden;
1543         $oldgrade->feedback       = $grade->feedback;
1544         $oldgrade->feedbackformat = $grade->feedbackformat;
1546         // MDL-31713 rawgramemin and max must be up to date so conditional access %'s works properly.
1547         $grade->rawgrademin = $this->grademin;
1548         $grade->rawgrademax = $this->grademax;
1549         $grade->rawscaleid  = $this->scaleid;
1551         // changed grade?
1552         if ($finalgrade !== false) {
1553             if ($this->is_overridable_item()) {
1554                 $grade->overridden = time();
1555             }
1557             $grade->finalgrade = $this->bounded_grade($finalgrade);
1558         }
1560         // do we have comment from teacher?
1561         if ($feedback !== false) {
1562             if ($this->is_overridable_item_feedback()) {
1563                 // external items (modules, plugins) may have own feedback
1564                 $grade->overridden = time();
1565             }
1567             $grade->feedback       = $feedback;
1568             $grade->feedbackformat = $feedbackformat;
1569         }
1571         if (empty($grade->id)) {
1572             $grade->timecreated  = null;   // hack alert - date submitted - no submission yet
1573             $grade->timemodified = time(); // hack alert - date graded
1574             $result = (bool)$grade->insert($source);
1576         } else if (grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)
1577                 or $grade->feedback       !== $oldgrade->feedback
1578                 or $grade->feedbackformat != $oldgrade->feedbackformat
1579                 or ($oldgrade->overridden == 0 and $grade->overridden > 0)) {
1580             $grade->timemodified = time(); // hack alert - date graded
1581             $result = $grade->update($source);
1582         } else {
1583             // no grade change
1584             return $result;
1585         }
1587         if (!$result) {
1588             // something went wrong - better force final grade recalculation
1589             $this->force_regrading();
1591         } else if ($this->is_course_item() and !$this->needsupdate) {
1592             if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
1593                 $this->force_regrading();
1594             }
1596         } else if (!$this->needsupdate) {
1597             $course_item = grade_item::fetch_course_item($this->courseid);
1598             if (!$course_item->needsupdate) {
1599                 if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
1600                     $this->force_regrading();
1601                 }
1602             } else {
1603                 $this->force_regrading();
1604             }
1605         }
1607         return $result;
1608     }
1611     /**
1612      * Updates raw grade value for given user, this is a only way to update raw
1613      * grades from external source (modules, etc.),
1614      * because it logs the change in history table and deals with final grade recalculation.
1615      *
1616      * @param int $userid the graded user
1617      * @param mixed $rawgrade float value of raw grade - false means do not change
1618      * @param string $source modification source
1619      * @param string $feedback optional teacher feedback
1620      * @param int $feedbackformat A format like FORMAT_PLAIN or FORMAT_HTML
1621      * @param int $usermodified the ID of the user who did the grading
1622      * @param int $dategraded A timestamp of when the student's work was graded
1623      * @param int $datesubmitted A timestamp of when the student's work was submitted
1624      * @param grade_grade $grade A grade object, useful for bulk upgrades
1625      * @return bool success
1626      */
1627     public function update_raw_grade($userid, $rawgrade=false, $source=NULL, $feedback=false, $feedbackformat=FORMAT_MOODLE, $usermodified=null, $dategraded=null, $datesubmitted=null, $grade=null) {
1628         global $USER;
1630         $result = true;
1632         // calculated grades can not be updated; course and category can not be updated  because they are aggregated
1633         if (!$this->is_raw_used() or $this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) {
1634             return false;
1635         }
1637         if (is_null($grade)) {
1638             //fetch from db
1639             $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid));
1640         }
1641         $grade->grade_item =& $this; // prevent db fetching of this grade_item
1643         if (empty($usermodified)) {
1644             $grade->usermodified = $USER->id;
1645         } else {
1646             $grade->usermodified = $usermodified;
1647         }
1649         if ($grade->is_locked()) {
1650             // do not update locked grades at all
1651             return false;
1652         }
1654         $locktime = $grade->get_locktime();
1655         if ($locktime and $locktime < time()) {
1656             // do not update grades that should be already locked and force regrade
1657             $this->force_regrading();
1658             return false;
1659         }
1661         $oldgrade = new stdClass();
1662         $oldgrade->finalgrade     = $grade->finalgrade;
1663         $oldgrade->rawgrade       = $grade->rawgrade;
1664         $oldgrade->rawgrademin    = $grade->rawgrademin;
1665         $oldgrade->rawgrademax    = $grade->rawgrademax;
1666         $oldgrade->rawscaleid     = $grade->rawscaleid;
1667         $oldgrade->feedback       = $grade->feedback;
1668         $oldgrade->feedbackformat = $grade->feedbackformat;
1670         // use new min and max
1671         $grade->rawgrade    = $grade->rawgrade;
1672         $grade->rawgrademin = $this->grademin;
1673         $grade->rawgrademax = $this->grademax;
1674         $grade->rawscaleid  = $this->scaleid;
1676         // change raw grade?
1677         if ($rawgrade !== false) {
1678             $grade->rawgrade = $rawgrade;
1679         }
1681         // empty feedback means no feedback at all
1682         if ($feedback === '') {
1683             $feedback = null;
1684         }
1686         // do we have comment from teacher?
1687         if ($feedback !== false and !$grade->is_overridden()) {
1688             $grade->feedback       = $feedback;
1689             $grade->feedbackformat = $feedbackformat;
1690         }
1692         // update final grade if possible
1693         if (!$grade->is_locked() and !$grade->is_overridden()) {
1694             $grade->finalgrade = $this->adjust_raw_grade($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
1695         }
1697         // TODO: hack alert - create new fields for these in 2.0
1698         $oldgrade->timecreated  = $grade->timecreated;
1699         $oldgrade->timemodified = $grade->timemodified;
1701         $grade->timecreated = $datesubmitted;
1703         if ($grade->is_overridden()) {
1704             // keep original graded date - update_final_grade() sets this for overridden grades
1706         } else if (is_null($grade->rawgrade) and is_null($grade->feedback)) {
1707             // no grade and feedback means no grading yet
1708             $grade->timemodified = null;
1710         } else if (!empty($dategraded)) {
1711             // fine - module sends info when graded (yay!)
1712             $grade->timemodified = $dategraded;
1714         } else if (grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)
1715                    or $grade->feedback !== $oldgrade->feedback) {
1716             // guess - if either grade or feedback changed set new graded date
1717             $grade->timemodified = time();
1719         } else {
1720             //keep original graded date
1721         }
1722         // end of hack alert
1724         if (empty($grade->id)) {
1725             $result = (bool)$grade->insert($source);
1727         } else if (grade_floats_different($grade->finalgrade,  $oldgrade->finalgrade)
1728                 or grade_floats_different($grade->rawgrade,    $oldgrade->rawgrade)
1729                 or grade_floats_different($grade->rawgrademin, $oldgrade->rawgrademin)
1730                 or grade_floats_different($grade->rawgrademax, $oldgrade->rawgrademax)
1731                 or $grade->rawscaleid     != $oldgrade->rawscaleid
1732                 or $grade->feedback       !== $oldgrade->feedback
1733                 or $grade->feedbackformat != $oldgrade->feedbackformat
1734                 or $grade->timecreated    != $oldgrade->timecreated  // part of hack above
1735                 or $grade->timemodified   != $oldgrade->timemodified // part of hack above
1736                 ) {
1737             $result = $grade->update($source);
1738         } else {
1739             return $result;
1740         }
1742         if (!$result) {
1743             // something went wrong - better force final grade recalculation
1744             $this->force_regrading();
1746         } else if (!$this->needsupdate) {
1747             $course_item = grade_item::fetch_course_item($this->courseid);
1748             if (!$course_item->needsupdate) {
1749                 if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
1750                     $this->force_regrading();
1751                 }
1752             }
1753         }
1755         return $result;
1756     }
1758     /**
1759      * Calculates final grade values using the formula in the calculation property.
1760      * The parameters are taken from final grades of grade items in current course only.
1761      *
1762      * @param int $userid Supply a user ID to limit the calculations to the grades of a single user
1763      * @return bool false if error
1764      */
1765     public function compute($userid=null) {
1766         global $CFG, $DB;
1768         if (!$this->is_calculated()) {
1769             return false;
1770         }
1772         require_once($CFG->libdir.'/mathslib.php');
1774         if ($this->is_locked()) {
1775             return true; // no need to recalculate locked items
1776         }
1778         // Precreate grades - we need them to exist
1779         if ($userid) {
1780             $missing = array();
1781             if (!$DB->record_exists('grade_grades', array('itemid'=>$this->id, 'userid'=>$userid))) {
1782                 $m = new stdClass();
1783                 $m->userid = $userid;
1784                 $missing[] = $m;
1785             }
1786         } else {
1787             // Find any users who have grades for some but not all grade items in this course
1788             $params = array('gicourseid' => $this->courseid, 'ggitemid' => $this->id);
1789             $sql = "SELECT gg.userid
1790                       FROM {grade_grades} gg
1791                            JOIN {grade_items} gi
1792                            ON (gi.id = gg.itemid AND gi.courseid = :gicourseid)
1793                      GROUP BY gg.userid
1794                      HAVING SUM(CASE WHEN gg.itemid = :ggitemid THEN 1 ELSE 0 END) = 0";
1795             $missing = $DB->get_records_sql($sql, $params);
1796         }
1798         if ($missing) {
1799             foreach ($missing as $m) {
1800                 $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$m->userid), false);
1801                 $grade->grade_item =& $this;
1802                 $grade->insert('system');
1803             }
1804         }
1806         // get used items
1807         $useditems = $this->depends_on();
1809         // prepare formula and init maths library
1810         $formula = preg_replace('/##(gi\d+)##/', '\1', $this->calculation);
1811         if (strpos($formula, '[[') !== false) {
1812             // missing item
1813             return false;
1814         }
1815         $this->formula = new calc_formula($formula);
1817         // where to look for final grades?
1818         // this itemid is added so that we use only one query for source and final grades
1819         $gis = array_merge($useditems, array($this->id));
1820         list($usql, $params) = $DB->get_in_or_equal($gis);
1822         if ($userid) {
1823             $usersql = "AND g.userid=?";
1824             $params[] = $userid;
1825         } else {
1826             $usersql = "";
1827         }
1829         $grade_inst = new grade_grade();
1830         $fields = 'g.'.implode(',g.', $grade_inst->required_fields);
1832         $params[] = $this->courseid;
1833         $sql = "SELECT $fields
1834                   FROM {grade_grades} g, {grade_items} gi
1835                  WHERE gi.id = g.itemid AND gi.id $usql $usersql AND gi.courseid=?
1836                  ORDER BY g.userid";
1838         $return = true;
1840         // group the grades by userid and use formula on the group
1841         $rs = $DB->get_recordset_sql($sql, $params);
1842         if ($rs->valid()) {
1843             $prevuser = 0;
1844             $grade_records   = array();
1845             $oldgrade    = null;
1846             foreach ($rs as $used) {
1847                 if ($used->userid != $prevuser) {
1848                     if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {
1849                         $return = false;
1850                     }
1851                     $prevuser = $used->userid;
1852                     $grade_records   = array();
1853                     $oldgrade    = null;
1854                 }
1855                 if ($used->itemid == $this->id) {
1856                     $oldgrade = $used;
1857                 }
1858                 $grade_records['gi'.$used->itemid] = $used->finalgrade;
1859             }
1860             if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {
1861                 $return = false;
1862             }
1863         }
1864         $rs->close();
1866         return $return;
1867     }
1869     /**
1870      * Internal function that does the final grade calculation
1871      *
1872      * @param int $userid The user ID
1873      * @param array $params An array of grade items of the form {'gi'.$itemid]} => $finalgrade
1874      * @param array $useditems An array of grade item IDs that this grade item depends on plus its own ID
1875      * @param grade_grade $oldgrade A grade_grade instance containing the old values from the database
1876      * @return bool False if an error occurred
1877      */
1878     public function use_formula($userid, $params, $useditems, $oldgrade) {
1879         if (empty($userid)) {
1880             return true;
1881         }
1883         // add missing final grade values
1884         // not graded (null) is counted as 0 - the spreadsheet way
1885         $allinputsnull = true;
1886         foreach($useditems as $gi) {
1887             if (!array_key_exists('gi'.$gi, $params) || is_null($params['gi'.$gi])) {
1888                 $params['gi'.$gi] = 0;
1889             } else {
1890                 $params['gi'.$gi] = (float)$params['gi'.$gi];
1891                 if ($gi != $this->id) {
1892                     $allinputsnull = false;
1893                 }
1894             }
1895         }
1897         // can not use own final grade during calculation
1898         unset($params['gi'.$this->id]);
1900         // insert final grade - will be needed later anyway
1901         if ($oldgrade) {
1902             $oldfinalgrade = $oldgrade->finalgrade;
1903             $grade = new grade_grade($oldgrade, false); // fetching from db is not needed
1904             $grade->grade_item =& $this;
1906         } else {
1907             $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid), false);
1908             $grade->grade_item =& $this;
1909             $grade->insert('system');
1910             $oldfinalgrade = null;
1911         }
1913         // no need to recalculate locked or overridden grades
1914         if ($grade->is_locked() or $grade->is_overridden()) {
1915             return true;
1916         }
1918         if ($allinputsnull) {
1919             $grade->finalgrade = null;
1920             $result = true;
1922         } else {
1924             // do the calculation
1925             $this->formula->set_params($params);
1926             $result = $this->formula->evaluate();
1928             if ($result === false) {
1929                 $grade->finalgrade = null;
1931             } else {
1932                 // normalize
1933                 $grade->finalgrade = $this->bounded_grade($result);
1934             }
1936         }
1938         // update in db if changed
1939         if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) {
1940             $grade->timemodified = time();
1941             $grade->update('compute');
1942         }
1944         if ($result !== false) {
1945             //lock grade if needed
1946         }
1948         if ($result === false) {
1949             return false;
1950         } else {
1951             return true;
1952         }
1954     }
1956     /**
1957      * Validate the formula.
1958      *
1959      * @param string $formulastr
1960      * @return bool true if calculation possible, false otherwise
1961      */
1962     public function validate_formula($formulastr) {
1963         global $CFG, $DB;
1964         require_once($CFG->libdir.'/mathslib.php');
1966         $formulastr = grade_item::normalize_formula($formulastr, $this->courseid);
1968         if (empty($formulastr)) {
1969             return true;
1970         }
1972         if (strpos($formulastr, '=') !== 0) {
1973             return get_string('errorcalculationnoequal', 'grades');
1974         }
1976         // get used items
1977         if (preg_match_all('/##gi(\d+)##/', $formulastr, $matches)) {
1978             $useditems = array_unique($matches[1]); // remove duplicates
1979         } else {
1980             $useditems = array();
1981         }
1983         // MDL-11902
1984         // unset the value if formula is trying to reference to itself
1985         // but array keys does not match itemid
1986         if (!empty($this->id)) {
1987             $useditems = array_diff($useditems, array($this->id));
1988             //unset($useditems[$this->id]);
1989         }
1991         // prepare formula and init maths library
1992         $formula = preg_replace('/##(gi\d+)##/', '\1', $formulastr);
1993         $formula = new calc_formula($formula);
1996         if (empty($useditems)) {
1997             $grade_items = array();
1999         } else {
2000             list($usql, $params) = $DB->get_in_or_equal($useditems);
2001             $params[] = $this->courseid;
2002             $sql = "SELECT gi.*
2003                       FROM {grade_items} gi
2004                      WHERE gi.id $usql and gi.courseid=?"; // from the same course only!
2006             if (!$grade_items = $DB->get_records_sql($sql, $params)) {
2007                 $grade_items = array();
2008             }
2009         }
2011         $params = array();
2012         foreach ($useditems as $itemid) {
2013             // make sure all grade items exist in this course
2014             if (!array_key_exists($itemid, $grade_items)) {
2015                 return false;
2016             }
2017             // use max grade when testing formula, this should be ok in 99.9%
2018             // division by 0 is one of possible problems
2019             $params['gi'.$grade_items[$itemid]->id] = $grade_items[$itemid]->grademax;
2020         }
2022         // do the calculation
2023         $formula->set_params($params);
2024         $result = $formula->evaluate();
2026         // false as result indicates some problem
2027         if ($result === false) {
2028             // TODO: add more error hints
2029             return get_string('errorcalculationunknown', 'grades');
2030         } else {
2031             return true;
2032         }
2033     }
2035     /**
2036      * Returns the value of the display type
2037      *
2038      * It can be set at 3 levels: grade_item, course setting and site. The lowest level overrides the higher ones.
2039      *
2040      * @return int Display type
2041      */
2042     public function get_displaytype() {
2043         global $CFG;
2045         if ($this->display == GRADE_DISPLAY_TYPE_DEFAULT) {
2046             return grade_get_setting($this->courseid, 'displaytype', $CFG->grade_displaytype);
2048         } else {
2049             return $this->display;
2050         }
2051     }
2053     /**
2054      * Returns the value of the decimals field
2055      *
2056      * It can be set at 3 levels: grade_item, course setting and site. The lowest level overrides the higher ones.
2057      *
2058      * @return int Decimals (0 - 5)
2059      */
2060     public function get_decimals() {
2061         global $CFG;
2063         if (is_null($this->decimals)) {
2064             return grade_get_setting($this->courseid, 'decimalpoints', $CFG->grade_decimalpoints);
2066         } else {
2067             return $this->decimals;
2068         }
2069     }
2071     /**
2072      * Returns a string representing the range of grademin - grademax for this grade item.
2073      *
2074      * @param int $rangesdisplaytype
2075      * @param int $rangesdecimalpoints
2076      * @return string
2077      */
2078     function get_formatted_range($rangesdisplaytype=null, $rangesdecimalpoints=null) {
2080         global $USER;
2082         // Determine which display type to use for this average
2083         if (isset($USER->gradeediting) && array_key_exists($this->courseid, $USER->gradeediting) && $USER->gradeediting[$this->courseid]) {
2084             $displaytype = GRADE_DISPLAY_TYPE_REAL;
2086         } else if ($rangesdisplaytype == GRADE_REPORT_PREFERENCE_INHERIT) { // no ==0 here, please resave report and user prefs
2087             $displaytype = $this->get_displaytype();
2089         } else {
2090             $displaytype = $rangesdisplaytype;
2091         }
2093         // Override grade_item setting if a display preference (not default) was set for the averages
2094         if ($rangesdecimalpoints == GRADE_REPORT_PREFERENCE_INHERIT) {
2095             $decimalpoints = $this->get_decimals();
2097         } else {
2098             $decimalpoints = $rangesdecimalpoints;
2099         }
2101         if ($displaytype == GRADE_DISPLAY_TYPE_PERCENTAGE) {
2102             $grademin = "0 %";
2103             $grademax = "100 %";
2105         } else {
2106             $grademin = grade_format_gradevalue($this->grademin, $this, true, $displaytype, $decimalpoints);
2107             $grademax = grade_format_gradevalue($this->grademax, $this, true, $displaytype, $decimalpoints);
2108         }
2110         return $grademin.'&ndash;'. $grademax;
2111     }
2113     /**
2114      * Queries parent categories recursively to find the aggregationcoef type that applies to this grade item.
2115      *
2116      * @return string|false Returns the coefficient string of false is no coefficient is being used
2117      */
2118     public function get_coefstring() {
2119         $parent_category = $this->load_parent_category();
2120         if ($this->is_category_item()) {
2121             $parent_category = $parent_category->load_parent_category();
2122         }
2124         if ($parent_category->is_aggregationcoef_used()) {
2125             return $parent_category->get_coefstring();
2126         } else {
2127             return false;
2128         }
2129     }
2131     /**
2132      * Returns whether the grade item can control the visibility of the grades
2133      *
2134      * @return bool
2135      */
2136     public function can_control_visibility() {
2137         if (core_component::get_plugin_directory($this->itemtype, $this->itemmodule)) {
2138             return !plugin_supports($this->itemtype, $this->itemmodule, FEATURE_CONTROLS_GRADE_VISIBILITY, false);
2139         }
2140         return parent::can_control_visibility();
2141     }
2143     /**
2144      * Used to notify the completion system (if necessary) that a user's grade
2145      * has changed, and clear up a possible score cache.
2146      *
2147      * @param bool $deleted True if grade was actually deleted
2148      */
2149     protected function notify_changed($deleted) {
2150         global $CFG;
2152         // Condition code may cache the grades for conditional availability of
2153         // modules or sections. (This code should use a hook for communication
2154         // with plugin, but hooks are not implemented at time of writing.)
2155         if (!empty($CFG->enableavailability) && class_exists('\availability_grade\callbacks')) {
2156             \availability_grade\callbacks::grade_item_changed($this->courseid);
2157         }
2158     }