Moodle release 4.0.11
[moodle.git] / lib / grade / grade_item.php
blob0e2d6165f41696b96c5f50766aae43de7cac81dd
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.
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
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
26 defined('MOODLE_INTERNAL') || die();
27 require_once('grade_object.php');
29 /**
30 * Class representing a grade item.
32 * It is responsible for handling its DB representation, modifying and returning its metadata.
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
39 class grade_item extends grade_object {
40 /**
41 * DB Table (used by grade_object).
42 * @var string $table
44 public $table = 'grade_items';
46 /**
47 * Array of required table fields, must start with 'id'.
48 * @var array $required_fields
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
60 public $courseid;
62 /**
63 * The category this grade_item belongs to (optional).
64 * @var int $categoryid
66 public $categoryid;
68 /**
69 * The grade_category object referenced $this->iteminstance if itemtype == 'category' or == 'course'.
70 * @var grade_category $item_category
72 public $item_category;
74 /**
75 * The grade_category object referenced by $this->categoryid.
76 * @var grade_category $parent_category
78 public $parent_category;
81 /**
82 * The name of this grade_item (pushed by the module).
83 * @var string $itemname
85 public $itemname;
87 /**
88 * e.g. 'category', 'course' and 'mod', 'blocks', 'import', etc...
89 * @var string $itemtype
91 public $itemtype;
93 /**
94 * The module pushing this grade (e.g. 'forum', 'quiz', 'assignment' etc).
95 * @var string $itemmodule
97 public $itemmodule;
99 /**
100 * ID of the item module
101 * @var int $iteminstance
103 public $iteminstance;
106 * Number of the item in a series of multiple grades pushed by an activity.
107 * @var int $itemnumber
109 public $itemnumber;
112 * Info and notes about this item.
113 * @var string $iteminfo
115 public $iteminfo;
118 * Arbitrary idnumber provided by the module responsible.
119 * @var string $idnumber
121 public $idnumber;
124 * Calculation string used for this item.
125 * @var string $calculation
127 public $calculation;
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
134 public $calculation_normalized;
136 * Math evaluation object
137 * @var calc_formula A formula object
139 public $formula;
142 * The type of grade (0 = none, 1 = value, 2 = scale, 3 = text)
143 * @var int $gradetype
145 public $gradetype = GRADE_TYPE_VALUE;
148 * Maximum allowable grade.
149 * @var float $grademax
151 public $grademax = 100;
154 * Minimum allowable grade.
155 * @var float $grademin
157 public $grademin = 0;
160 * id of the scale, if this grade is based on a scale.
161 * @var int $scaleid
163 public $scaleid;
166 * The grade_scale object referenced by $this->scaleid.
167 * @var grade_scale $scale
169 public $scale;
172 * The id of the optional grade_outcome associated with this grade_item.
173 * @var int $outcomeid
175 public $outcomeid;
178 * The grade_outcome this grade is associated with, if applicable.
179 * @var grade_outcome $outcome
181 public $outcome;
184 * grade required to pass. (grademin <= gradepass <= grademax)
185 * @var float $gradepass
187 public $gradepass = 0;
190 * Multiply all grades by this number.
191 * @var float $multfactor
193 public $multfactor = 1.0;
196 * Add this to all grades.
197 * @var float $plusfactor
199 public $plusfactor = 0;
202 * Aggregation coeficient used for weighted averages or extra credit
203 * @var float $aggregationcoef
205 public $aggregationcoef = 0;
208 * Aggregation coeficient used for weighted averages only
209 * @var float $aggregationcoef2
211 public $aggregationcoef2 = 0;
214 * Sorting order of the columns.
215 * @var int $sortorder
217 public $sortorder = 0;
220 * Display type of the grades (Real, Percentage, Letter, or default).
221 * @var int $display
223 public $display = GRADE_DISPLAY_TYPE_DEFAULT;
226 * The number of digits after the decimal point symbol. Applies only to REAL and PERCENTAGE grade display types.
227 * @var int $decimals
229 public $decimals = null;
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
235 public $locked = 0;
238 * Date after which the grade will be locked. Empty means no automatic locking.
239 * @var int $locktime
241 public $locktime = 0;
244 * If set, the whole column will be recalculated, then this flag will be switched off.
245 * @var bool $needsupdate
247 public $needsupdate = 1;
250 * If set, the grade item's weight has been overridden by a user and should not be automatically adjusted.
252 public $weightoverride = 0;
255 * Cached dependson array
256 * @var array An array of cached grade item dependencies.
258 public $dependson_cache = null;
261 * @var bool If we regrade this item should we mark it as overridden?
263 public $markasoverriddenwhengraded = true;
266 * Constructor. Optionally (and by default) attempts to fetch corresponding row from the database
268 * @param array $params An array with required parameters for this grade object.
269 * @param bool $fetch Whether to fetch corresponding row from the database or not,
270 * optional fields might not be defined if false used
272 public function __construct($params = null, $fetch = true) {
273 global $CFG;
274 // Set grademax from $CFG->gradepointdefault .
275 self::set_properties($this, array('grademax' => $CFG->gradepointdefault));
276 parent::__construct($params, $fetch);
280 * In addition to update() as defined in grade_object, handle the grade_outcome and grade_scale objects.
281 * Force regrading if necessary, rounds the float numbers using php function,
282 * the reason is we need to compare the db value with computed number to skip regrading if possible.
284 * @param string $source from where was the object inserted (mod/forum, manual, etc.)
285 * @param bool $isbulkupdate If bulk grade update is happening.
286 * @return bool success
288 public function update($source = null, $isbulkupdate = false) {
289 // reset caches
290 $this->dependson_cache = null;
292 // Retrieve scale and infer grademax/min from it if needed
293 $this->load_scale();
295 // make sure there is not 0 in outcomeid
296 if (empty($this->outcomeid)) {
297 $this->outcomeid = null;
300 if ($this->qualifies_for_regrading()) {
301 $this->force_regrading();
304 $this->timemodified = time();
306 $this->grademin = grade_floatval($this->grademin);
307 $this->grademax = grade_floatval($this->grademax);
308 $this->multfactor = grade_floatval($this->multfactor);
309 $this->plusfactor = grade_floatval($this->plusfactor);
310 $this->aggregationcoef = grade_floatval($this->aggregationcoef);
311 $this->aggregationcoef2 = grade_floatval($this->aggregationcoef2);
313 $result = parent::update($source, $isbulkupdate);
315 if ($result) {
316 $event = \core\event\grade_item_updated::create_from_grade_item($this);
317 $event->trigger();
320 return $result;
324 * Compares the values held by this object with those of the matching record in DB, and returns
325 * whether or not these differences are sufficient to justify an update of all parent objects.
326 * This assumes that this object has an id number and a matching record in DB. If not, it will return false.
328 * @return bool
330 public function qualifies_for_regrading() {
331 if (empty($this->id)) {
332 return false;
335 $db_item = new grade_item(array('id' => $this->id));
337 $calculationdiff = $db_item->calculation != $this->calculation;
338 $categorydiff = $db_item->categoryid != $this->categoryid;
339 $gradetypediff = $db_item->gradetype != $this->gradetype;
340 $scaleiddiff = $db_item->scaleid != $this->scaleid;
341 $outcomeiddiff = $db_item->outcomeid != $this->outcomeid;
342 $locktimediff = $db_item->locktime != $this->locktime;
343 $grademindiff = grade_floats_different($db_item->grademin, $this->grademin);
344 $grademaxdiff = grade_floats_different($db_item->grademax, $this->grademax);
345 $multfactordiff = grade_floats_different($db_item->multfactor, $this->multfactor);
346 $plusfactordiff = grade_floats_different($db_item->plusfactor, $this->plusfactor);
347 $acoefdiff = grade_floats_different($db_item->aggregationcoef, $this->aggregationcoef);
348 $acoefdiff2 = grade_floats_different($db_item->aggregationcoef2, $this->aggregationcoef2);
349 $weightoverride = grade_floats_different($db_item->weightoverride, $this->weightoverride);
351 $needsupdatediff = !$db_item->needsupdate && $this->needsupdate; // force regrading only if setting the flag first time
352 $lockeddiff = !empty($db_item->locked) && empty($this->locked); // force regrading only when unlocking
354 return ($calculationdiff || $categorydiff || $gradetypediff || $grademaxdiff || $grademindiff || $scaleiddiff
355 || $outcomeiddiff || $multfactordiff || $plusfactordiff || $needsupdatediff
356 || $lockeddiff || $acoefdiff || $acoefdiff2 || $weightoverride || $locktimediff);
360 * Finds and returns a grade_item instance based on params.
362 * @static
363 * @param array $params associative arrays varname=>value
364 * @return grade_item|bool Returns a grade_item instance or false if none found
366 public static function fetch($params) {
367 return grade_object::fetch_helper('grade_items', 'grade_item', $params);
371 * Check to see if there are any existing grades for this grade_item.
373 * @return boolean - true if there are valid grades for this grade_item.
375 public function has_grades() {
376 global $DB;
378 $count = $DB->count_records_select('grade_grades',
379 'itemid = :gradeitemid AND finalgrade IS NOT NULL',
380 array('gradeitemid' => $this->id));
381 return $count > 0;
385 * Check to see if there are existing overridden grades for this grade_item.
387 * @return boolean - true if there are overridden grades for this grade_item.
389 public function has_overridden_grades() {
390 global $DB;
392 $count = $DB->count_records_select('grade_grades',
393 'itemid = :gradeitemid AND finalgrade IS NOT NULL AND overridden > 0',
394 array('gradeitemid' => $this->id));
395 return $count > 0;
399 * Finds and returns all grade_item instances based on params.
401 * @static
402 * @param array $params associative arrays varname=>value
403 * @return array array of grade_item instances or false if none found.
405 public static function fetch_all($params) {
406 return grade_object::fetch_all_helper('grade_items', 'grade_item', $params);
410 * Delete all grades and force_regrading of parent category.
412 * @param string $source from where was the object deleted (mod/forum, manual, etc.)
413 * @return bool success
415 public function delete($source=null) {
416 global $DB;
418 $transaction = $DB->start_delegated_transaction();
419 $this->delete_all_grades($source);
420 $success = parent::delete($source);
421 $transaction->allow_commit();
423 if ($success) {
424 $event = \core\event\grade_item_deleted::create_from_grade_item($this);
425 $event->trigger();
428 return $success;
432 * Delete all grades
434 * @param string $source from where was the object deleted (mod/forum, manual, etc.)
435 * @return bool
437 public function delete_all_grades($source=null) {
438 global $DB;
440 $transaction = $DB->start_delegated_transaction();
442 if (!$this->is_course_item()) {
443 $this->force_regrading();
446 if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
447 foreach ($grades as $grade) {
448 $grade->delete($source);
452 // Delete all the historical files.
453 // We only support feedback files for modules atm.
454 if ($this->is_external_item()) {
455 $fs = new file_storage();
456 $fs->delete_area_files($this->get_context()->id, GRADE_FILE_COMPONENT, GRADE_HISTORY_FEEDBACK_FILEAREA);
459 $transaction->allow_commit();
461 return true;
465 * Duplicate grade item.
467 * @return grade_item The duplicate grade item
469 public function duplicate() {
470 // Convert current object to array.
471 $copy = (array) $this;
473 if (empty($copy["id"])) {
474 throw new moodle_exception('invalidgradeitemid');
477 // Remove fields that will be either unique or automatically filled.
478 $removekeys = array();
479 $removekeys[] = 'id';
480 $removekeys[] = 'idnumber';
481 $removekeys[] = 'timecreated';
482 $removekeys[] = 'sortorder';
483 foreach ($removekeys as $key) {
484 unset($copy[$key]);
487 // Addendum to name.
488 $copy["itemname"] = get_string('duplicatedgradeitem', 'grades', $copy["itemname"]);
490 // Create new grade item.
491 $gradeitem = new grade_item($copy);
493 // Insert grade item into database.
494 $gradeitem->insert();
496 return $gradeitem;
500 * In addition to perform parent::insert(), calls force_regrading() method too.
502 * @param string $source From where was the object inserted (mod/forum, manual, etc.)
503 * @param string $isbulkupdate If bulk grade update is happening.
504 * @return int PK ID if successful, false otherwise
506 public function insert($source = null, $isbulkupdate = false) {
507 global $CFG, $DB;
509 if (empty($this->courseid)) {
510 print_error('cannotinsertgrade');
513 // load scale if needed
514 $this->load_scale();
516 // add parent category if needed
517 if (empty($this->categoryid) and !$this->is_course_item() and !$this->is_category_item()) {
518 $course_category = grade_category::fetch_course_category($this->courseid);
519 $this->categoryid = $course_category->id;
523 // always place the new items at the end, move them after insert if needed
524 $last_sortorder = $DB->get_field_select('grade_items', 'MAX(sortorder)', "courseid = ?", array($this->courseid));
525 if (!empty($last_sortorder)) {
526 $this->sortorder = $last_sortorder + 1;
527 } else {
528 $this->sortorder = 1;
531 // add proper item numbers to manual items
532 if ($this->itemtype == 'manual') {
533 if (empty($this->itemnumber)) {
534 $this->itemnumber = 0;
538 // make sure there is not 0 in outcomeid
539 if (empty($this->outcomeid)) {
540 $this->outcomeid = null;
543 $this->timecreated = $this->timemodified = time();
545 if (parent::insert($source, $isbulkupdate)) {
546 // force regrading of items if needed
547 $this->force_regrading();
549 $event = \core\event\grade_item_created::create_from_grade_item($this);
550 $event->trigger();
552 return $this->id;
554 } else {
555 debugging("Could not insert this grade_item in the database!");
556 return false;
561 * Set idnumber of grade item, updates also course_modules table
563 * @param string $idnumber (without magic quotes)
564 * @return bool success
566 public function add_idnumber($idnumber) {
567 global $DB;
568 if (!empty($this->idnumber)) {
569 return false;
572 if ($this->itemtype == 'mod' and !$this->is_outcome_item()) {
573 if ($this->itemnumber == 0) {
574 // for activity modules, itemnumber 0 is synced with the course_modules
575 if (!$cm = get_coursemodule_from_instance($this->itemmodule, $this->iteminstance, $this->courseid)) {
576 return false;
578 if (!empty($cm->idnumber)) {
579 return false;
581 $DB->set_field('course_modules', 'idnumber', $idnumber, array('id' => $cm->id));
582 $this->idnumber = $idnumber;
583 return $this->update();
584 } else {
585 $this->idnumber = $idnumber;
586 return $this->update();
589 } else {
590 $this->idnumber = $idnumber;
591 return $this->update();
596 * Returns the locked state of this grade_item (if the grade_item is locked OR no specific
597 * $userid is given) or the locked state of a specific grade within this item if a specific
598 * $userid is given and the grade_item is unlocked.
600 * @param int $userid The user's ID
601 * @return bool Locked state
603 public function is_locked($userid=NULL) {
604 global $CFG;
606 // Override for any grade items belonging to activities which are in the process of being deleted.
607 require_once($CFG->dirroot . '/course/lib.php');
608 if (course_module_instance_pending_deletion($this->courseid, $this->itemmodule, $this->iteminstance)) {
609 return true;
612 if (!empty($this->locked)) {
613 return true;
616 if (!empty($userid)) {
617 if ($grade = grade_grade::fetch(array('itemid'=>$this->id, 'userid'=>$userid))) {
618 $grade->grade_item =& $this; // prevent db fetching of cached grade_item
619 return $grade->is_locked();
623 return false;
627 * Locks or unlocks this grade_item and (optionally) all its associated final grades.
629 * @param int $lockedstate 0, 1 or a timestamp int(10) after which date the item will be locked.
630 * @param bool $cascade Lock/unlock child objects too
631 * @param bool $refresh Refresh grades when unlocking
632 * @return bool True if grade_item all grades updated, false if at least one update fails
634 public function set_locked($lockedstate, $cascade=false, $refresh=true) {
635 if ($lockedstate) {
636 /// setting lock
637 if ($this->needsupdate) {
638 return false; // can not lock grade without first having final grade
641 $this->locked = time();
642 $this->update();
644 if ($cascade) {
645 $grades = $this->get_final();
646 foreach($grades as $g) {
647 $grade = new grade_grade($g, false);
648 $grade->grade_item =& $this;
649 $grade->set_locked(1, null, false);
653 return true;
655 } else {
656 /// removing lock
657 if (!empty($this->locked) and $this->locktime < time()) {
658 //we have to reset locktime or else it would lock up again
659 $this->locktime = 0;
662 $this->locked = 0;
663 $this->update();
665 if ($cascade) {
666 if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
667 foreach($grades as $grade) {
668 $grade->grade_item =& $this;
669 $grade->set_locked(0, null, false);
674 if ($refresh) {
675 //refresh when unlocking
676 $this->refresh_grades();
679 return true;
684 * Lock the grade if needed. Make sure this is called only when final grades are valid
686 public function check_locktime() {
687 if (!empty($this->locked)) {
688 return; // already locked
691 if ($this->locktime and $this->locktime < time()) {
692 $this->locked = time();
693 $this->update('locktime');
698 * Set the locktime for this grade item.
700 * @param int $locktime timestamp for lock to activate
701 * @return void
703 public function set_locktime($locktime) {
704 $this->locktime = $locktime;
705 $this->update();
709 * Set the locktime for this grade item.
711 * @return int $locktime timestamp for lock to activate
713 public function get_locktime() {
714 return $this->locktime;
718 * Set the hidden status of grade_item and all grades.
720 * 0 mean always visible, 1 means always hidden and a number > 1 is a timestamp to hide until
722 * @param int $hidden new hidden status
723 * @param bool $cascade apply to child objects too
725 public function set_hidden($hidden, $cascade=false) {
726 parent::set_hidden($hidden, $cascade);
728 if ($cascade) {
729 if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
730 foreach($grades as $grade) {
731 $grade->grade_item =& $this;
732 $grade->set_hidden($hidden, $cascade);
737 //if marking item visible make sure category is visible MDL-21367
738 if( !$hidden ) {
739 $category_array = grade_category::fetch_all(array('id'=>$this->categoryid));
740 if ($category_array && array_key_exists($this->categoryid, $category_array)) {
741 $category = $category_array[$this->categoryid];
742 //call set_hidden on the category regardless of whether it is hidden as its parent might be hidden
743 $category->set_hidden($hidden, false);
749 * Returns the number of grades that are hidden
751 * @param string $groupsql SQL to limit the query by group
752 * @param array $params SQL params for $groupsql
753 * @param string $groupwheresql Where conditions for $groupsql
754 * @return int The number of hidden grades
756 public function has_hidden_grades($groupsql="", array $params=null, $groupwheresql="") {
757 global $DB;
758 $params = (array)$params;
759 $params['itemid'] = $this->id;
761 return $DB->get_field_sql("SELECT COUNT(*) FROM {grade_grades} g LEFT JOIN "
762 ."{user} u ON g.userid = u.id $groupsql WHERE itemid = :itemid AND hidden = 1 $groupwheresql", $params);
766 * Mark regrading as finished successfully. This will also be called when subsequent regrading will not change any grades.
767 * Situations such as an error being found will still result in the regrading being finished.
769 public function regrading_finished() {
770 global $DB;
771 $this->needsupdate = 0;
772 //do not use $this->update() because we do not want this logged in grade_item_history
773 $DB->set_field('grade_items', 'needsupdate', 0, array('id' => $this->id));
777 * Performs the necessary calculations on the grades_final referenced by this grade_item.
778 * Also resets the needsupdate flag once successfully performed.
780 * This function must be used ONLY from lib/gradeslib.php/grade_regrade_final_grades(),
781 * because the regrading must be done in correct order!!
783 * @param int $userid Supply a user ID to limit the regrading to a single user
784 * @param \core\progress\base|null $progress Optional progress object, will be updated per user
785 * @return bool true if ok, error string otherwise
787 public function regrade_final_grades($userid=null, ?\core\progress\base $progress = null) {
788 global $CFG, $DB;
790 // locked grade items already have correct final grades
791 if ($this->is_locked()) {
792 return true;
795 // calculation produces final value using formula from other final values
796 if ($this->is_calculated()) {
797 if ($this->compute($userid)) {
798 return true;
799 } else {
800 return "Could not calculate grades for grade item"; // TODO: improve and localize
803 // noncalculated outcomes already have final values - raw grades not used
804 } else if ($this->is_outcome_item()) {
805 return true;
807 // aggregate the category grade
808 } else if ($this->is_category_item() or $this->is_course_item()) {
809 // aggregate category grade item
810 $category = $this->load_item_category();
811 $category->grade_item =& $this;
812 if ($category->generate_grades($userid, $progress)) {
813 return true;
814 } else {
815 return "Could not aggregate final grades for category:".$this->id; // TODO: improve and localize
818 } else if ($this->is_manual_item()) {
819 // manual items track only final grades, no raw grades
820 return true;
822 } else if (!$this->is_raw_used()) {
823 // hmm - raw grades are not used- nothing to regrade
824 return true;
827 // normal grade item - just new final grades
828 $result = true;
829 $grade_inst = new grade_grade();
830 $fields = implode(',', $grade_inst->required_fields);
831 if ($userid) {
832 $params = array($this->id, $userid);
833 $rs = $DB->get_recordset_select('grade_grades', "itemid=? AND userid=?", $params, '', $fields);
834 } else {
835 $rs = $DB->get_recordset('grade_grades', array('itemid' => $this->id), '', $fields);
837 if ($rs) {
838 foreach ($rs as $grade_record) {
839 $grade = new grade_grade($grade_record, false);
841 // Incrementing the progress by nothing causes it to send an update (once per second)
842 // to the web browser so as to prevent the connection timing out.
843 if ($progress) {
844 $progress->increment_progress(0);
847 if (!empty($grade_record->locked) or !empty($grade_record->overridden)) {
848 // this grade is locked - final grade must be ok
849 continue;
852 $grade->finalgrade = $this->adjust_raw_grade($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
854 if (grade_floats_different($grade_record->finalgrade, $grade->finalgrade)) {
855 $success = $grade->update('system');
857 // If successful trigger a user_graded event.
858 if ($success) {
859 $grade->load_grade_item();
860 \core\event\user_graded::create_from_grade($grade, \core\event\base::USER_OTHER)->trigger();
861 } else {
862 $result = "Internal error updating final grade";
866 $rs->close();
869 return $result;
873 * Given a float grade value or integer grade scale, applies a number of adjustment based on
874 * grade_item variables and returns the result.
876 * @param float $rawgrade The raw grade value
877 * @param float $rawmin original rawmin
878 * @param float $rawmax original rawmax
879 * @return mixed
881 public function adjust_raw_grade($rawgrade, $rawmin, $rawmax) {
882 if (is_null($rawgrade)) {
883 return null;
886 if ($this->gradetype == GRADE_TYPE_VALUE) { // Dealing with numerical grade
888 if ($this->grademax < $this->grademin) {
889 return null;
892 if ($this->grademax == $this->grademin) {
893 return $this->grademax; // no range
896 // Standardise score to the new grade range
897 // NOTE: skip if the activity provides a manual rescaling option.
898 $manuallyrescale = (component_callback_exists('mod_' . $this->itemmodule, 'rescale_activity_grades') !== false);
899 if (!$manuallyrescale && ($rawmin != $this->grademin or $rawmax != $this->grademax)) {
900 $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax);
903 // Apply other grade_item factors
904 $rawgrade *= $this->multfactor;
905 $rawgrade += $this->plusfactor;
907 return $this->bounded_grade($rawgrade);
909 } else if ($this->gradetype == GRADE_TYPE_SCALE) { // Dealing with a scale value
910 if (empty($this->scale)) {
911 $this->load_scale();
914 if ($this->grademax < 0) {
915 return null; // scale not present - no grade
918 if ($this->grademax == 0) {
919 return $this->grademax; // only one option
922 // Convert scale if needed
923 // NOTE: skip if the activity provides a manual rescaling option.
924 $manuallyrescale = (component_callback_exists('mod_' . $this->itemmodule, 'rescale_activity_grades') !== false);
925 if (!$manuallyrescale && ($rawmin != $this->grademin or $rawmax != $this->grademax)) {
926 // This should never happen because scales are locked if they are in use.
927 $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax);
930 return $this->bounded_grade($rawgrade);
933 } else if ($this->gradetype == GRADE_TYPE_TEXT or $this->gradetype == GRADE_TYPE_NONE) { // no value
934 // somebody changed the grading type when grades already existed
935 return null;
937 } else {
938 debugging("Unknown grade type");
939 return null;
944 * Update the rawgrademax and rawgrademin for all grade_grades records for this item.
945 * Scale every rawgrade to maintain the percentage. This function should be called
946 * after the gradeitem has been updated to the new min and max values.
948 * @param float $oldgrademin The previous grade min value
949 * @param float $oldgrademax The previous grade max value
950 * @param float $newgrademin The new grade min value
951 * @param float $newgrademax The new grade max value
952 * @param string $source from where was the object inserted (mod/forum, manual, etc.)
953 * @return bool True on success
955 public function rescale_grades_keep_percentage($oldgrademin, $oldgrademax, $newgrademin, $newgrademax, $source = null) {
956 global $DB;
958 if (empty($this->id)) {
959 return false;
962 if ($oldgrademax <= $oldgrademin) {
963 // Grades cannot be scaled.
964 return false;
966 $scale = ($newgrademax - $newgrademin) / ($oldgrademax - $oldgrademin);
967 if (($newgrademax - $newgrademin) <= 1) {
968 // We would lose too much precision, lets bail.
969 return false;
972 $rs = $DB->get_recordset('grade_grades', array('itemid' => $this->id));
974 foreach ($rs as $graderecord) {
975 // For each record, create an object to work on.
976 $grade = new grade_grade($graderecord, false);
977 // Set this object in the item so it doesn't re-fetch it.
978 $grade->grade_item = $this;
980 if (!$this->is_category_item() || ($this->is_category_item() && $grade->is_overridden())) {
981 // Updating the raw grade automatically updates the min/max.
982 if ($this->is_raw_used()) {
983 $rawgrade = (($grade->rawgrade - $oldgrademin) * $scale) + $newgrademin;
984 $this->update_raw_grade(false, $rawgrade, $source, false, FORMAT_MOODLE, null, null, null, $grade);
985 } else {
986 $finalgrade = (($grade->finalgrade - $oldgrademin) * $scale) + $newgrademin;
987 $this->update_final_grade($grade->userid, $finalgrade, $source);
991 $rs->close();
993 // Mark this item for regrading.
994 $this->force_regrading();
996 return true;
1000 * Sets this grade_item's needsupdate to true. Also marks the course item as needing update.
1002 * @return void
1004 public function force_regrading() {
1005 global $DB;
1006 $this->needsupdate = 1;
1007 //mark this item and course item only - categories and calculated items are always regraded
1008 $wheresql = "(itemtype='course' OR id=?) AND courseid=?";
1009 $params = array($this->id, $this->courseid);
1010 $DB->set_field_select('grade_items', 'needsupdate', 1, $wheresql, $params);
1014 * Instantiates a grade_scale object from the DB if this item's scaleid variable is set
1016 * @return grade_scale Returns a grade_scale object or null if no scale used
1018 public function load_scale() {
1019 if ($this->gradetype != GRADE_TYPE_SCALE) {
1020 $this->scaleid = null;
1023 if (!empty($this->scaleid)) {
1024 //do not load scale if already present
1025 if (empty($this->scale->id) or $this->scale->id != $this->scaleid) {
1026 $this->scale = grade_scale::fetch(array('id'=>$this->scaleid));
1027 if (!$this->scale) {
1028 debugging('Incorrect scale id: '.$this->scaleid);
1029 $this->scale = null;
1030 return null;
1032 $this->scale->load_items();
1035 // Until scales are uniformly set to min=0 max=count(scaleitems)-1 throughout Moodle, we
1036 // stay with the current min=1 max=count(scaleitems)
1037 $this->grademax = count($this->scale->scale_items);
1038 $this->grademin = 1;
1040 } else {
1041 $this->scale = null;
1044 return $this->scale;
1048 * Instantiates a grade_outcome object from the DB if this item's outcomeid variable is set
1050 * @return grade_outcome This grade item's associated grade_outcome or null
1052 public function load_outcome() {
1053 if (!empty($this->outcomeid)) {
1054 $this->outcome = grade_outcome::fetch(array('id'=>$this->outcomeid));
1056 return $this->outcome;
1060 * Returns the grade_category object this grade_item belongs to (referenced by categoryid)
1061 * or category attached to category item.
1063 * @return grade_category|bool Returns a grade_category object if applicable or false if this is a course item
1065 public function get_parent_category() {
1066 if ($this->is_category_item() or $this->is_course_item()) {
1067 return $this->get_item_category();
1069 } else {
1070 return grade_category::fetch(array('id'=>$this->categoryid));
1075 * Calls upon the get_parent_category method to retrieve the grade_category object
1076 * from the DB and assigns it to $this->parent_category. It also returns the object.
1078 * @return grade_category This grade item's parent grade_category.
1080 public function load_parent_category() {
1081 if (empty($this->parent_category->id)) {
1082 $this->parent_category = $this->get_parent_category();
1084 return $this->parent_category;
1088 * Returns the grade_category for a grade category grade item
1090 * @return grade_category|bool Returns a grade_category instance if applicable or false otherwise
1092 public function get_item_category() {
1093 if (!$this->is_course_item() and !$this->is_category_item()) {
1094 return false;
1096 return grade_category::fetch(array('id'=>$this->iteminstance));
1100 * Calls upon the get_item_category method to retrieve the grade_category object
1101 * from the DB and assigns it to $this->item_category. It also returns the object.
1103 * @return grade_category
1105 public function load_item_category() {
1106 if (empty($this->item_category->id)) {
1107 $this->item_category = $this->get_item_category();
1109 return $this->item_category;
1113 * Is the grade item associated with category?
1115 * @return bool
1117 public function is_category_item() {
1118 return ($this->itemtype == 'category');
1122 * Is the grade item associated with course?
1124 * @return bool
1126 public function is_course_item() {
1127 return ($this->itemtype == 'course');
1131 * Is this a manually graded item?
1133 * @return bool
1135 public function is_manual_item() {
1136 return ($this->itemtype == 'manual');
1140 * Is this an outcome item?
1142 * @return bool
1144 public function is_outcome_item() {
1145 return !empty($this->outcomeid);
1149 * Is the grade item external - associated with module, plugin or something else?
1151 * @return bool
1153 public function is_external_item() {
1154 return ($this->itemtype == 'mod');
1158 * Is the grade item overridable
1160 * @return bool
1162 public function is_overridable_item() {
1163 if ($this->is_course_item() or $this->is_category_item()) {
1164 $overridable = (bool) get_config('moodle', 'grade_overridecat');
1165 } else {
1166 $overridable = false;
1169 return !$this->is_outcome_item() and ($this->is_external_item() or $this->is_calculated() or $overridable);
1173 * Is the grade item feedback overridable
1175 * @return bool
1177 public function is_overridable_item_feedback() {
1178 return !$this->is_outcome_item() and $this->is_external_item();
1182 * Returns true if grade items uses raw grades
1184 * @return bool
1186 public function is_raw_used() {
1187 return ($this->is_external_item() and !$this->is_calculated() and !$this->is_outcome_item());
1191 * Returns true if the grade item is an aggreggated type grade.
1193 * @since Moodle 2.8.7, 2.9.1
1194 * @return bool
1196 public function is_aggregate_item() {
1197 return ($this->is_category_item() || $this->is_course_item());
1201 * Returns the grade item associated with the course
1203 * @param int $courseid
1204 * @return grade_item Course level grade item object
1206 public static function fetch_course_item($courseid) {
1207 if ($course_item = grade_item::fetch(array('courseid'=>$courseid, 'itemtype'=>'course'))) {
1208 return $course_item;
1211 // first get category - it creates the associated grade item
1212 $course_category = grade_category::fetch_course_category($courseid);
1213 return $course_category->get_grade_item();
1217 * Is grading object editable?
1219 * @return bool
1221 public function is_editable() {
1222 return true;
1226 * Checks if grade calculated. Returns this object's calculation.
1228 * @return bool true if grade item calculated.
1230 public function is_calculated() {
1231 if (empty($this->calculation)) {
1232 return false;
1236 * The main reason why we use the ##gixxx## instead of [[idnumber]] is speed of depends_on(),
1237 * we would have to fetch all course grade items to find out the ids.
1238 * Also if user changes the idnumber the formula does not need to be updated.
1241 // first detect if we need to change calculation formula from [[idnumber]] to ##giXXX## (after backup, etc.)
1242 if (!$this->calculation_normalized and strpos($this->calculation, '[[') !== false) {
1243 $this->set_calculation($this->calculation);
1246 return !empty($this->calculation);
1250 * Returns calculation string if grade calculated.
1252 * @return string Returns the grade item's calculation if calculation is used, null if not
1254 public function get_calculation() {
1255 if ($this->is_calculated()) {
1256 return grade_item::denormalize_formula($this->calculation, $this->courseid);
1258 } else {
1259 return NULL;
1264 * Sets this item's calculation (creates it) if not yet set, or
1265 * updates it if already set (in the DB). If no calculation is given,
1266 * the calculation is removed.
1268 * @param string $formula string representation of formula used for calculation
1269 * @return bool success
1271 public function set_calculation($formula) {
1272 $this->calculation = grade_item::normalize_formula($formula, $this->courseid);
1273 $this->calculation_normalized = true;
1274 return $this->update();
1278 * Denormalizes the calculation formula to [idnumber] form
1280 * @param string $formula A string representation of the formula
1281 * @param int $courseid The course ID
1282 * @return string The denormalized formula as a string
1284 public static function denormalize_formula($formula, $courseid) {
1285 if (empty($formula)) {
1286 return '';
1289 // denormalize formula - convert ##giXX## to [[idnumber]]
1290 if (preg_match_all('/##gi(\d+)##/', $formula, $matches)) {
1291 foreach ($matches[1] as $id) {
1292 if ($grade_item = grade_item::fetch(array('id'=>$id, 'courseid'=>$courseid))) {
1293 if (!empty($grade_item->idnumber)) {
1294 $formula = str_replace('##gi'.$grade_item->id.'##', '[['.$grade_item->idnumber.']]', $formula);
1300 return $formula;
1305 * Normalizes the calculation formula to [#giXX#] form
1307 * @param string $formula The formula
1308 * @param int $courseid The course ID
1309 * @return string The normalized formula as a string
1311 public static function normalize_formula($formula, $courseid) {
1312 $formula = trim($formula);
1314 if (empty($formula)) {
1315 return NULL;
1319 // normalize formula - we want grade item ids ##giXXX## instead of [[idnumber]]
1320 if ($grade_items = grade_item::fetch_all(array('courseid'=>$courseid))) {
1321 foreach ($grade_items as $grade_item) {
1322 $formula = str_replace('[['.$grade_item->idnumber.']]', '##gi'.$grade_item->id.'##', $formula);
1326 return $formula;
1330 * Returns the final values for this grade item (as imported by module or other source).
1332 * @param int $userid Optional: to retrieve a single user's final grade
1333 * @return array|grade_grade An array of all grade_grade instances for this grade_item, or a single grade_grade instance.
1335 public function get_final($userid=NULL) {
1336 global $DB;
1337 if ($userid) {
1338 if ($user = $DB->get_record('grade_grades', array('itemid' => $this->id, 'userid' => $userid))) {
1339 return $user;
1342 } else {
1343 if ($grades = $DB->get_records('grade_grades', array('itemid' => $this->id))) {
1344 //TODO: speed up with better SQL (MDL-31380)
1345 $result = array();
1346 foreach ($grades as $grade) {
1347 $result[$grade->userid] = $grade;
1349 return $result;
1350 } else {
1351 return array();
1357 * Get (or create if not exist yet) grade for this user
1359 * @param int $userid The user ID
1360 * @param bool $create If true and the user has no grade for this grade item a new grade_grade instance will be inserted
1361 * @return grade_grade The grade_grade instance for the user for this grade item
1363 public function get_grade($userid, $create=true) {
1364 if (empty($this->id)) {
1365 debugging('Can not use before insert');
1366 return false;
1369 $grade = new grade_grade(array('userid'=>$userid, 'itemid'=>$this->id));
1370 if (empty($grade->id) and $create) {
1371 $grade->insert();
1374 return $grade;
1378 * Returns the sortorder of this grade_item. This method is also available in
1379 * grade_category, for cases where the object type is not know.
1381 * @return int Sort order
1383 public function get_sortorder() {
1384 return $this->sortorder;
1388 * Returns the idnumber of this grade_item. This method is also available in
1389 * grade_category, for cases where the object type is not know.
1391 * @return string The grade item idnumber
1393 public function get_idnumber() {
1394 return $this->idnumber;
1398 * Returns this grade_item. This method is also available in
1399 * grade_category, for cases where the object type is not know.
1401 * @return grade_item
1403 public function get_grade_item() {
1404 return $this;
1408 * Sets the sortorder of this grade_item. This method is also available in
1409 * grade_category, for cases where the object type is not know.
1411 * @param int $sortorder
1413 public function set_sortorder($sortorder) {
1414 if ($this->sortorder == $sortorder) {
1415 return;
1417 $this->sortorder = $sortorder;
1418 $this->update();
1422 * Update this grade item's sortorder so that it will appear after $sortorder
1424 * @param int $sortorder The sort order to place this grade item after
1426 public function move_after_sortorder($sortorder) {
1427 global $CFG, $DB;
1429 //make some room first
1430 $params = array($sortorder, $this->courseid);
1431 $sql = "UPDATE {grade_items}
1432 SET sortorder = sortorder + 1
1433 WHERE sortorder > ? AND courseid = ?";
1434 $DB->execute($sql, $params);
1436 $this->set_sortorder($sortorder + 1);
1440 * Detect duplicate grade item's sortorder and re-sort them.
1441 * Note: Duplicate sortorder will be introduced while duplicating activities or
1442 * merging two courses.
1444 * @param int $courseid id of the course for which grade_items sortorder need to be fixed.
1446 public static function fix_duplicate_sortorder($courseid) {
1447 global $DB;
1449 $transaction = $DB->start_delegated_transaction();
1451 $sql = "SELECT DISTINCT g1.id, g1.courseid, g1.sortorder
1452 FROM {grade_items} g1
1453 JOIN {grade_items} g2 ON g1.courseid = g2.courseid
1454 WHERE g1.sortorder = g2.sortorder AND g1.id != g2.id AND g1.courseid = :courseid
1455 ORDER BY g1.sortorder DESC, g1.id DESC";
1457 // Get all duplicates in course highest sort order, and higest id first so that we can make space at the
1458 // bottom higher end of the sort orders and work down by id.
1459 $rs = $DB->get_recordset_sql($sql, array('courseid' => $courseid));
1461 foreach($rs as $duplicate) {
1462 $DB->execute("UPDATE {grade_items}
1463 SET sortorder = sortorder + 1
1464 WHERE courseid = :courseid AND
1465 (sortorder > :sortorder OR (sortorder = :sortorder2 AND id > :id))",
1466 array('courseid' => $duplicate->courseid,
1467 'sortorder' => $duplicate->sortorder,
1468 'sortorder2' => $duplicate->sortorder,
1469 'id' => $duplicate->id));
1471 $rs->close();
1472 $transaction->allow_commit();
1476 * Returns the most descriptive field for this object.
1478 * Determines what type of grade item it is then returns the appropriate string
1480 * @param bool $fulltotal If the item is a category total, returns $categoryname."total" instead of "Category total" or "Course total"
1481 * @param bool $escape Whether the returned category name is to be HTML escaped or not.
1482 * @return string name
1484 public function get_name($fulltotal=false, $escape = true) {
1485 global $CFG;
1486 require_once($CFG->dirroot . '/course/lib.php');
1487 if (strval($this->itemname) !== '') {
1488 // MDL-10557
1490 // Make it obvious to users if the course module to which this grade item relates, is currently being removed.
1491 $deletionpending = course_module_instance_pending_deletion($this->courseid, $this->itemmodule, $this->iteminstance);
1492 $deletionnotice = get_string('gradesmoduledeletionprefix', 'grades');
1494 $options = ['context' => context_course::instance($this->courseid), 'escape' => $escape];
1495 return $deletionpending ?
1496 format_string($deletionnotice . ' ' . $this->itemname, true, $options) :
1497 format_string($this->itemname, true, $options);
1499 } else if ($this->is_course_item()) {
1500 return get_string('coursetotal', 'grades');
1502 } else if ($this->is_category_item()) {
1503 if ($fulltotal) {
1504 $category = $this->load_parent_category();
1505 $a = new stdClass();
1506 $a->category = $category->get_name($escape);
1507 return get_string('categorytotalfull', 'grades', $a);
1508 } else {
1509 return get_string('categorytotal', 'grades');
1512 } else {
1513 return get_string('gradenoun');
1518 * A grade item can return a more detailed description which will be added to the header of the column/row in some reports.
1520 * @return string description
1522 public function get_description() {
1523 if ($this->is_course_item() || $this->is_category_item()) {
1524 $categoryitem = $this->load_item_category();
1525 return $categoryitem->get_description();
1527 return '';
1531 * Sets this item's categoryid. A generic method shared by objects that have a parent id of some kind.
1533 * @param int $parentid The ID of the new parent
1534 * @param bool $updateaggregationfields Whether or not to convert the aggregation fields when switching between category.
1535 * Set this to false when the aggregation fields have been updated in prevision of the new
1536 * category, typically when the item is freshly created.
1537 * @return bool True if success
1539 public function set_parent($parentid, $updateaggregationfields = true) {
1540 if ($this->is_course_item() or $this->is_category_item()) {
1541 print_error('cannotsetparentforcatoritem');
1544 if ($this->categoryid == $parentid) {
1545 return true;
1548 // find parent and check course id
1549 if (!$parent_category = grade_category::fetch(array('id'=>$parentid, 'courseid'=>$this->courseid))) {
1550 return false;
1553 $currentparent = $this->load_parent_category();
1555 if ($updateaggregationfields) {
1556 $this->set_aggregation_fields_for_aggregation($currentparent->aggregation, $parent_category->aggregation);
1559 $this->force_regrading();
1561 // set new parent
1562 $this->categoryid = $parent_category->id;
1563 $this->parent_category =& $parent_category;
1565 return $this->update();
1569 * Update the aggregation fields when the aggregation changed.
1571 * This method should always be called when the aggregation has changed, but also when
1572 * the item was moved to another category, even it if uses the same aggregation method.
1574 * Some values such as the weight only make sense within a category, once moved the
1575 * values should be reset to let the user adapt them accordingly.
1577 * Note that this method does not save the grade item.
1578 * {@link grade_item::update()} has to be called manually after using this method.
1580 * @param int $from Aggregation method constant value.
1581 * @param int $to Aggregation method constant value.
1582 * @return boolean True when at least one field was changed, false otherwise
1584 public function set_aggregation_fields_for_aggregation($from, $to) {
1585 $defaults = grade_category::get_default_aggregation_coefficient_values($to);
1587 $origaggregationcoef = $this->aggregationcoef;
1588 $origaggregationcoef2 = $this->aggregationcoef2;
1589 $origweighoverride = $this->weightoverride;
1591 if ($from == GRADE_AGGREGATE_SUM && $to == GRADE_AGGREGATE_SUM && $this->weightoverride) {
1592 // Do nothing. We are switching from SUM to SUM and the weight is overriden,
1593 // a teacher would not expect any change in this situation.
1595 } else if ($from == GRADE_AGGREGATE_WEIGHTED_MEAN && $to == GRADE_AGGREGATE_WEIGHTED_MEAN) {
1596 // Do nothing. The weights can be kept in this case.
1598 } else if (in_array($from, array(GRADE_AGGREGATE_SUM, GRADE_AGGREGATE_EXTRACREDIT_MEAN, GRADE_AGGREGATE_WEIGHTED_MEAN2))
1599 && in_array($to, array(GRADE_AGGREGATE_SUM, GRADE_AGGREGATE_EXTRACREDIT_MEAN, GRADE_AGGREGATE_WEIGHTED_MEAN2))) {
1601 // Reset all but the the extra credit field.
1602 $this->aggregationcoef2 = $defaults['aggregationcoef2'];
1603 $this->weightoverride = $defaults['weightoverride'];
1605 if ($to != GRADE_AGGREGATE_EXTRACREDIT_MEAN) {
1606 // Normalise extra credit, except for 'Mean with extra credit' which supports higher values than 1.
1607 $this->aggregationcoef = min(1, $this->aggregationcoef);
1609 } else {
1610 // Reset all.
1611 $this->aggregationcoef = $defaults['aggregationcoef'];
1612 $this->aggregationcoef2 = $defaults['aggregationcoef2'];
1613 $this->weightoverride = $defaults['weightoverride'];
1616 $acoefdiff = grade_floats_different($origaggregationcoef, $this->aggregationcoef);
1617 $acoefdiff2 = grade_floats_different($origaggregationcoef2, $this->aggregationcoef2);
1618 $weightoverride = grade_floats_different($origweighoverride, $this->weightoverride);
1620 return $acoefdiff || $acoefdiff2 || $weightoverride;
1624 * Makes sure value is a valid grade value.
1626 * @param float $gradevalue
1627 * @return mixed float or int fixed grade value
1629 public function bounded_grade($gradevalue) {
1630 global $CFG;
1632 if (is_null($gradevalue)) {
1633 return null;
1636 if ($this->gradetype == GRADE_TYPE_SCALE) {
1637 // no >100% grades hack for scale grades!
1638 // 1.5 is rounded to 2 ;-)
1639 return (int)bounded_number($this->grademin, round($gradevalue+0.00001), $this->grademax);
1642 $grademax = $this->grademax;
1644 // NOTE: if you change this value you must manually reset the needsupdate flag in all grade items
1645 $maxcoef = isset($CFG->gradeoverhundredprocentmax) ? $CFG->gradeoverhundredprocentmax : 10; // 1000% max by default
1647 if (!empty($CFG->unlimitedgrades)) {
1648 // NOTE: if you change this value you must manually reset the needsupdate flag in all grade items
1649 $grademax = $grademax * $maxcoef;
1650 } else if ($this->is_category_item() or $this->is_course_item()) {
1651 $category = $this->load_item_category();
1652 if ($category->aggregation >= 100) {
1653 // grade >100% hack
1654 $grademax = $grademax * $maxcoef;
1658 return (float)bounded_number($this->grademin, $gradevalue, $grademax);
1662 * Finds out on which other items does this depend directly when doing calculation or category aggregation
1664 * @param bool $reset_cache
1665 * @return array of grade_item IDs this one depends on
1667 public function depends_on($reset_cache=false) {
1668 global $CFG, $DB;
1670 if ($reset_cache) {
1671 $this->dependson_cache = null;
1672 } else if (isset($this->dependson_cache)) {
1673 return $this->dependson_cache;
1676 if ($this->is_locked() && !$this->is_category_item()) {
1677 // locked items do not need to be regraded
1678 $this->dependson_cache = array();
1679 return $this->dependson_cache;
1682 if ($this->is_calculated()) {
1683 if (preg_match_all('/##gi(\d+)##/', $this->calculation, $matches)) {
1684 $this->dependson_cache = array_unique($matches[1]); // remove duplicates
1685 return $this->dependson_cache;
1686 } else {
1687 $this->dependson_cache = array();
1688 return $this->dependson_cache;
1691 } else if ($grade_category = $this->load_item_category()) {
1692 $params = array();
1694 //only items with numeric or scale values can be aggregated
1695 if ($this->gradetype != GRADE_TYPE_VALUE and $this->gradetype != GRADE_TYPE_SCALE) {
1696 $this->dependson_cache = array();
1697 return $this->dependson_cache;
1700 $grade_category->apply_forced_settings();
1702 if (empty($CFG->enableoutcomes) or $grade_category->aggregateoutcomes) {
1703 $outcomes_sql = "";
1704 } else {
1705 $outcomes_sql = "AND gi.outcomeid IS NULL";
1708 if (empty($CFG->grade_includescalesinaggregation)) {
1709 $gtypes = "gi.gradetype = ?";
1710 $params[] = GRADE_TYPE_VALUE;
1711 } else {
1712 $gtypes = "(gi.gradetype = ? OR gi.gradetype = ?)";
1713 $params[] = GRADE_TYPE_VALUE;
1714 $params[] = GRADE_TYPE_SCALE;
1717 $params[] = $grade_category->id;
1718 $params[] = $this->courseid;
1719 $params[] = $grade_category->id;
1720 $params[] = $this->courseid;
1721 if (empty($CFG->grade_includescalesinaggregation)) {
1722 $params[] = GRADE_TYPE_VALUE;
1723 } else {
1724 $params[] = GRADE_TYPE_VALUE;
1725 $params[] = GRADE_TYPE_SCALE;
1727 $sql = "SELECT gi.id
1728 FROM {grade_items} gi
1729 WHERE $gtypes
1730 AND gi.categoryid = ?
1731 AND gi.courseid = ?
1732 $outcomes_sql
1733 UNION
1735 SELECT gi.id
1736 FROM {grade_items} gi, {grade_categories} gc
1737 WHERE (gi.itemtype = 'category' OR gi.itemtype = 'course') AND gi.iteminstance=gc.id
1738 AND gc.parent = ?
1739 AND gi.courseid = ?
1740 AND $gtypes
1741 $outcomes_sql";
1743 if ($children = $DB->get_records_sql($sql, $params)) {
1744 $this->dependson_cache = array_keys($children);
1745 return $this->dependson_cache;
1746 } else {
1747 $this->dependson_cache = array();
1748 return $this->dependson_cache;
1751 } else {
1752 $this->dependson_cache = array();
1753 return $this->dependson_cache;
1758 * Refetch grades from modules, plugins.
1760 * @param int $userid optional, limit the refetch to a single user
1761 * @return bool Returns true on success or if there is nothing to do
1763 public function refresh_grades($userid=0) {
1764 global $DB;
1765 if ($this->itemtype == 'mod') {
1766 if ($this->is_outcome_item()) {
1767 //nothing to do
1768 return true;
1771 if (!$activity = $DB->get_record($this->itemmodule, array('id' => $this->iteminstance))) {
1772 debugging("Can not find $this->itemmodule activity with id $this->iteminstance");
1773 return false;
1776 if (!$cm = get_coursemodule_from_instance($this->itemmodule, $activity->id, $this->courseid)) {
1777 debugging('Can not find course module');
1778 return false;
1781 $activity->modname = $this->itemmodule;
1782 $activity->cmidnumber = $cm->idnumber;
1784 return grade_update_mod_grades($activity, $userid);
1787 return true;
1791 * Updates final grade value for given user, this is a only way to update final
1792 * grades from gradebook and import because it logs the change in history table
1793 * and deals with overridden flag. This flag is set to prevent later overriding
1794 * from raw grades submitted from modules.
1796 * @param int $userid The graded user
1797 * @param float|false $finalgrade The float value of final grade, false means do not change
1798 * @param string $source The modification source
1799 * @param string $feedback Optional teacher feedback
1800 * @param int $feedbackformat A format like FORMAT_PLAIN or FORMAT_HTML
1801 * @param int $usermodified The ID of the user making the modification
1802 * @param int $timemodified Optional parameter to set the time modified, if not present current time.
1803 * @param bool $isbulkupdate If bulk grade update is happening.
1804 * @return bool success
1806 public function update_final_grade($userid, $finalgrade = false, $source = null, $feedback = false,
1807 $feedbackformat = FORMAT_MOODLE, $usermodified = null, $timemodified = null, $isbulkupdate = false) {
1808 global $USER, $CFG;
1810 $result = true;
1812 // no grading used or locked
1813 if ($this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) {
1814 return false;
1817 $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid));
1818 $grade->grade_item =& $this; // prevent db fetching of this grade_item
1820 if (empty($usermodified)) {
1821 $grade->usermodified = $USER->id;
1822 } else {
1823 $grade->usermodified = $usermodified;
1826 if ($grade->is_locked()) {
1827 // do not update locked grades at all
1828 return false;
1831 $locktime = $grade->get_locktime();
1832 if ($locktime and $locktime < time()) {
1833 // do not update grades that should be already locked, force regrade instead
1834 $this->force_regrading();
1835 return false;
1838 $oldgrade = new stdClass();
1839 $oldgrade->finalgrade = $grade->finalgrade;
1840 $oldgrade->overridden = $grade->overridden;
1841 $oldgrade->feedback = $grade->feedback;
1842 $oldgrade->feedbackformat = $grade->feedbackformat;
1843 $oldgrade->rawgrademin = $grade->rawgrademin;
1844 $oldgrade->rawgrademax = $grade->rawgrademax;
1846 // MDL-31713 rawgramemin and max must be up to date so conditional access %'s works properly.
1847 $grade->rawgrademin = $this->grademin;
1848 $grade->rawgrademax = $this->grademax;
1849 $grade->rawscaleid = $this->scaleid;
1851 // changed grade?
1852 if ($finalgrade !== false) {
1853 if ($this->is_overridable_item() && $this->markasoverriddenwhengraded) {
1854 $grade->overridden = time();
1857 $grade->finalgrade = $this->bounded_grade($finalgrade);
1860 // do we have comment from teacher?
1861 if ($feedback !== false) {
1862 if ($this->is_overridable_item_feedback()) {
1863 // external items (modules, plugins) may have own feedback
1864 $grade->overridden = time();
1867 $grade->feedback = $feedback;
1868 $grade->feedbackformat = $feedbackformat;
1871 $gradechanged = false;
1872 if (empty($grade->id)) {
1873 $grade->timecreated = null; // Hack alert - date submitted - no submission yet.
1874 $grade->timemodified = $timemodified ?? time(); // Hack alert - date graded.
1875 $result = (bool)$grade->insert($source, $isbulkupdate);
1877 // If the grade insert was successful and the final grade was not null then trigger a user_graded event.
1878 if ($result && !is_null($grade->finalgrade)) {
1879 \core\event\user_graded::create_from_grade($grade)->trigger();
1881 $gradechanged = true;
1882 } else {
1883 // Existing grade_grades.
1885 if (grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)
1886 or grade_floats_different($grade->rawgrademin, $oldgrade->rawgrademin)
1887 or grade_floats_different($grade->rawgrademax, $oldgrade->rawgrademax)
1888 or ($oldgrade->overridden == 0 and $grade->overridden > 0)) {
1889 $gradechanged = true;
1892 if ($grade->feedback === $oldgrade->feedback and $grade->feedbackformat == $oldgrade->feedbackformat and
1893 $gradechanged === false) {
1894 // No grade nor feedback changed.
1895 return $result;
1898 $grade->timemodified = $timemodified ?? time(); // Hack alert - date graded.
1899 $result = $grade->update($source, $isbulkupdate);
1901 // If the grade update was successful and the actual grade has changed then trigger a user_graded event.
1902 if ($result && grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)) {
1903 \core\event\user_graded::create_from_grade($grade)->trigger();
1907 if (!$result) {
1908 // Something went wrong - better force final grade recalculation.
1909 $this->force_regrading();
1910 return $result;
1913 // If we are not updating grades we don't need to recalculate the whole course.
1914 if (!$gradechanged) {
1915 return $result;
1918 if ($this->is_course_item() and !$this->needsupdate) {
1919 if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
1920 $this->force_regrading();
1923 } else if (!$this->needsupdate) {
1925 $course_item = grade_item::fetch_course_item($this->courseid);
1926 if (!$course_item->needsupdate) {
1927 if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
1928 $this->force_regrading();
1930 } else {
1931 $this->force_regrading();
1935 return $result;
1940 * Updates raw grade value for given user, this is a only way to update raw
1941 * grades from external source (modules, etc.),
1942 * because it logs the change in history table and deals with final grade recalculation.
1944 * @param int $userid the graded user
1945 * @param mixed $rawgrade float value of raw grade - false means do not change
1946 * @param string $source modification source
1947 * @param string $feedback optional teacher feedback
1948 * @param int $feedbackformat A format like FORMAT_PLAIN or FORMAT_HTML
1949 * @param int $usermodified the ID of the user who did the grading
1950 * @param int $dategraded A timestamp of when the student's work was graded
1951 * @param int $datesubmitted A timestamp of when the student's work was submitted
1952 * @param grade_grade $grade A grade object, useful for bulk upgrades
1953 * @param array $feedbackfiles An array identifying the location of files we want to copy to the gradebook feedback area.
1954 * Example -
1956 * 'contextid' => 1,
1957 * 'component' => 'mod_xyz',
1958 * 'filearea' => 'mod_xyz_feedback',
1959 * 'itemid' => 2
1960 * ];
1961 * @param bool $isbulkupdate If bulk grade update is happening.
1962 * @return bool success
1964 public function update_raw_grade($userid, $rawgrade = false, $source = null, $feedback = false,
1965 $feedbackformat = FORMAT_MOODLE, $usermodified = null, $dategraded = null, $datesubmitted=null,
1966 $grade = null, array $feedbackfiles = [], $isbulkupdate = false) {
1967 global $USER;
1969 $result = true;
1971 // calculated grades can not be updated; course and category can not be updated because they are aggregated
1972 if (!$this->is_raw_used() or $this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) {
1973 return false;
1976 if (is_null($grade)) {
1977 //fetch from db
1978 $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid));
1980 $grade->grade_item =& $this; // prevent db fetching of this grade_item
1982 if (empty($usermodified)) {
1983 $grade->usermodified = $USER->id;
1984 } else {
1985 $grade->usermodified = $usermodified;
1988 if ($grade->is_locked()) {
1989 // do not update locked grades at all
1990 return false;
1993 $locktime = $grade->get_locktime();
1994 if ($locktime and $locktime < time()) {
1995 // do not update grades that should be already locked and force regrade
1996 $this->force_regrading();
1997 return false;
2000 $oldgrade = new stdClass();
2001 $oldgrade->finalgrade = $grade->finalgrade;
2002 $oldgrade->rawgrade = $grade->rawgrade;
2003 $oldgrade->rawgrademin = $grade->rawgrademin;
2004 $oldgrade->rawgrademax = $grade->rawgrademax;
2005 $oldgrade->rawscaleid = $grade->rawscaleid;
2006 $oldgrade->feedback = $grade->feedback;
2007 $oldgrade->feedbackformat = $grade->feedbackformat;
2009 // use new min and max
2010 $grade->rawgrade = $grade->rawgrade;
2011 $grade->rawgrademin = $this->grademin;
2012 $grade->rawgrademax = $this->grademax;
2013 $grade->rawscaleid = $this->scaleid;
2015 // change raw grade?
2016 if ($rawgrade !== false) {
2017 $grade->rawgrade = $rawgrade;
2020 // empty feedback means no feedback at all
2021 if ($feedback === '') {
2022 $feedback = null;
2025 // do we have comment from teacher?
2026 if ($feedback !== false and !$grade->is_overridden()) {
2027 $grade->feedback = $feedback;
2028 $grade->feedbackformat = $feedbackformat;
2029 $grade->feedbackfiles = $feedbackfiles;
2032 // update final grade if possible
2033 if (!$grade->is_locked() and !$grade->is_overridden()) {
2034 $grade->finalgrade = $this->adjust_raw_grade($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
2037 // TODO: hack alert - create new fields for these in 2.0
2038 $oldgrade->timecreated = $grade->timecreated;
2039 $oldgrade->timemodified = $grade->timemodified;
2041 $grade->timecreated = $datesubmitted;
2043 if ($grade->is_overridden()) {
2044 // keep original graded date - update_final_grade() sets this for overridden grades
2046 } else if (is_null($grade->rawgrade) and is_null($grade->feedback)) {
2047 // no grade and feedback means no grading yet
2048 $grade->timemodified = null;
2050 } else if (!empty($dategraded)) {
2051 // fine - module sends info when graded (yay!)
2052 $grade->timemodified = $dategraded;
2054 } else if (grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)
2055 or $grade->feedback !== $oldgrade->feedback) {
2056 // guess - if either grade or feedback changed set new graded date
2057 $grade->timemodified = time();
2059 } else {
2060 //keep original graded date
2062 // end of hack alert
2064 $gradechanged = false;
2065 if (empty($grade->id)) {
2066 $result = (bool)$grade->insert($source, $isbulkupdate);
2068 // If the grade insert was successful and the final grade was not null then trigger a user_graded event.
2069 if ($result && !is_null($grade->finalgrade)) {
2070 \core\event\user_graded::create_from_grade($grade)->trigger();
2072 $gradechanged = true;
2073 } else {
2074 // Existing grade_grades.
2076 if (grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)
2077 or grade_floats_different($grade->rawgrade, $oldgrade->rawgrade)
2078 or grade_floats_different($grade->rawgrademin, $oldgrade->rawgrademin)
2079 or grade_floats_different($grade->rawgrademax, $oldgrade->rawgrademax)
2080 or $grade->rawscaleid != $oldgrade->rawscaleid) {
2081 $gradechanged = true;
2084 // The timecreated and timemodified checking is part of the hack above.
2085 if ($gradechanged === false and
2086 $grade->feedback === $oldgrade->feedback and
2087 $grade->feedbackformat == $oldgrade->feedbackformat and
2088 $grade->timecreated == $oldgrade->timecreated and
2089 $grade->timemodified == $oldgrade->timemodified) {
2090 // No changes.
2091 return $result;
2093 $result = $grade->update($source, $isbulkupdate);
2095 // If the grade update was successful and the actual grade has changed then trigger a user_graded event.
2096 if ($result && grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)) {
2097 \core\event\user_graded::create_from_grade($grade)->trigger();
2101 if (!$result) {
2102 // Something went wrong - better force final grade recalculation.
2103 $this->force_regrading();
2104 return $result;
2107 // If we are not updating grades we don't need to recalculate the whole course.
2108 if (!$gradechanged) {
2109 return $result;
2112 if (!$this->needsupdate) {
2113 $course_item = grade_item::fetch_course_item($this->courseid);
2114 if (!$course_item->needsupdate) {
2115 if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
2116 $this->force_regrading();
2121 return $result;
2125 * Calculates final grade values using the formula in the calculation property.
2126 * The parameters are taken from final grades of grade items in current course only.
2128 * @param int $userid Supply a user ID to limit the calculations to the grades of a single user
2129 * @return bool false if error
2131 public function compute($userid=null) {
2132 global $CFG, $DB;
2134 if (!$this->is_calculated()) {
2135 return false;
2138 require_once($CFG->libdir.'/mathslib.php');
2140 if ($this->is_locked()) {
2141 return true; // no need to recalculate locked items
2144 // Precreate grades - we need them to exist
2145 if ($userid) {
2146 $missing = array();
2147 if (!$DB->record_exists('grade_grades', array('itemid'=>$this->id, 'userid'=>$userid))) {
2148 $m = new stdClass();
2149 $m->userid = $userid;
2150 $missing[] = $m;
2152 } else {
2153 // Find any users who have grades for some but not all grade items in this course
2154 $params = array('gicourseid' => $this->courseid, 'ggitemid' => $this->id);
2155 $sql = "SELECT gg.userid
2156 FROM {grade_grades} gg
2157 JOIN {grade_items} gi
2158 ON (gi.id = gg.itemid AND gi.courseid = :gicourseid)
2159 GROUP BY gg.userid
2160 HAVING SUM(CASE WHEN gg.itemid = :ggitemid THEN 1 ELSE 0 END) = 0";
2161 $missing = $DB->get_records_sql($sql, $params);
2164 if ($missing) {
2165 foreach ($missing as $m) {
2166 $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$m->userid), false);
2167 $grade->grade_item =& $this;
2168 $grade->insert('system');
2172 // get used items
2173 $useditems = $this->depends_on();
2175 // prepare formula and init maths library
2176 $formula = preg_replace('/##(gi\d+)##/', '\1', $this->calculation);
2177 if (strpos($formula, '[[') !== false) {
2178 // missing item
2179 return false;
2181 $this->formula = new calc_formula($formula);
2183 // where to look for final grades?
2184 // this itemid is added so that we use only one query for source and final grades
2185 $gis = array_merge($useditems, array($this->id));
2186 list($usql, $params) = $DB->get_in_or_equal($gis);
2188 if ($userid) {
2189 $usersql = "AND g.userid=?";
2190 $params[] = $userid;
2191 } else {
2192 $usersql = "";
2195 $grade_inst = new grade_grade();
2196 $fields = 'g.'.implode(',g.', $grade_inst->required_fields);
2198 $params[] = $this->courseid;
2199 $sql = "SELECT $fields
2200 FROM {grade_grades} g, {grade_items} gi
2201 WHERE gi.id = g.itemid AND gi.id $usql $usersql AND gi.courseid=?
2202 ORDER BY g.userid";
2204 $return = true;
2206 // group the grades by userid and use formula on the group
2207 $rs = $DB->get_recordset_sql($sql, $params);
2208 if ($rs->valid()) {
2209 $prevuser = 0;
2210 $grade_records = array();
2211 $oldgrade = null;
2212 foreach ($rs as $used) {
2213 if ($used->userid != $prevuser) {
2214 if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {
2215 $return = false;
2217 $prevuser = $used->userid;
2218 $grade_records = array();
2219 $oldgrade = null;
2221 if ($used->itemid == $this->id) {
2222 $oldgrade = $used;
2224 $grade_records['gi'.$used->itemid] = $used->finalgrade;
2226 if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {
2227 $return = false;
2230 $rs->close();
2232 return $return;
2236 * Internal function that does the final grade calculation
2238 * @param int $userid The user ID
2239 * @param array $params An array of grade items of the form {'gi'.$itemid]} => $finalgrade
2240 * @param array $useditems An array of grade item IDs that this grade item depends on plus its own ID
2241 * @param grade_grade $oldgrade A grade_grade instance containing the old values from the database
2242 * @return bool False if an error occurred
2244 public function use_formula($userid, $params, $useditems, $oldgrade) {
2245 if (empty($userid)) {
2246 return true;
2249 // add missing final grade values
2250 // not graded (null) is counted as 0 - the spreadsheet way
2251 $allinputsnull = true;
2252 foreach($useditems as $gi) {
2253 if (!array_key_exists('gi'.$gi, $params) || is_null($params['gi'.$gi])) {
2254 $params['gi'.$gi] = 0;
2255 } else {
2256 $params['gi'.$gi] = (float)$params['gi'.$gi];
2257 if ($gi != $this->id) {
2258 $allinputsnull = false;
2263 // can not use own final grade during calculation
2264 unset($params['gi'.$this->id]);
2266 // Check to see if the gradebook is frozen. This allows grades to not be altered at all until a user verifies that they
2267 // wish to update the grades.
2268 $gradebookcalculationsfreeze = get_config('core', 'gradebook_calculations_freeze_' . $this->courseid);
2270 $rawminandmaxchanged = false;
2271 // insert final grade - will be needed later anyway
2272 if ($oldgrade) {
2273 // Only run through this code if the gradebook isn't frozen.
2274 if ($gradebookcalculationsfreeze && (int)$gradebookcalculationsfreeze <= 20150627) {
2275 // Do nothing.
2276 } else {
2277 // The grade_grade for a calculated item should have the raw grade maximum and minimum set to the
2278 // grade_item grade maximum and minimum respectively.
2279 if ($oldgrade->rawgrademax != $this->grademax || $oldgrade->rawgrademin != $this->grademin) {
2280 $rawminandmaxchanged = true;
2281 $oldgrade->rawgrademax = $this->grademax;
2282 $oldgrade->rawgrademin = $this->grademin;
2285 $oldfinalgrade = $oldgrade->finalgrade;
2286 $grade = new grade_grade($oldgrade, false); // fetching from db is not needed
2287 $grade->grade_item =& $this;
2289 } else {
2290 $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid), false);
2291 $grade->grade_item =& $this;
2292 $rawminandmaxchanged = false;
2293 if ($gradebookcalculationsfreeze && (int)$gradebookcalculationsfreeze <= 20150627) {
2294 // Do nothing.
2295 } else {
2296 // The grade_grade for a calculated item should have the raw grade maximum and minimum set to the
2297 // grade_item grade maximum and minimum respectively.
2298 $rawminandmaxchanged = true;
2299 $grade->rawgrademax = $this->grademax;
2300 $grade->rawgrademin = $this->grademin;
2302 $grade->insert('system');
2303 $oldfinalgrade = null;
2306 // no need to recalculate locked or overridden grades
2307 if ($grade->is_locked() or $grade->is_overridden()) {
2308 return true;
2311 if ($allinputsnull) {
2312 $grade->finalgrade = null;
2313 $result = true;
2315 } else {
2317 // do the calculation
2318 $this->formula->set_params($params);
2319 $result = $this->formula->evaluate();
2321 if ($result === false) {
2322 $grade->finalgrade = null;
2324 } else {
2325 // normalize
2326 $grade->finalgrade = $this->bounded_grade($result);
2330 // Only run through this code if the gradebook isn't frozen.
2331 if ($gradebookcalculationsfreeze && (int)$gradebookcalculationsfreeze <= 20150627) {
2332 // Update in db if changed.
2333 if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) {
2334 $grade->timemodified = time();
2335 $success = $grade->update('compute');
2337 // If successful trigger a user_graded event.
2338 if ($success) {
2339 \core\event\user_graded::create_from_grade($grade)->trigger();
2342 } else {
2343 // Update in db if changed.
2344 if (grade_floats_different($grade->finalgrade, $oldfinalgrade) || $rawminandmaxchanged) {
2345 $grade->timemodified = time();
2346 $success = $grade->update('compute');
2348 // If successful trigger a user_graded event.
2349 if ($success) {
2350 \core\event\user_graded::create_from_grade($grade)->trigger();
2355 if ($result !== false) {
2356 //lock grade if needed
2359 if ($result === false) {
2360 return false;
2361 } else {
2362 return true;
2368 * Validate the formula.
2370 * @param string $formulastr
2371 * @return bool true if calculation possible, false otherwise
2373 public function validate_formula($formulastr) {
2374 global $CFG, $DB;
2375 require_once($CFG->libdir.'/mathslib.php');
2377 $formulastr = grade_item::normalize_formula($formulastr, $this->courseid);
2379 if (empty($formulastr)) {
2380 return true;
2383 if (strpos($formulastr, '=') !== 0) {
2384 return get_string('errorcalculationnoequal', 'grades');
2387 // get used items
2388 if (preg_match_all('/##gi(\d+)##/', $formulastr, $matches)) {
2389 $useditems = array_unique($matches[1]); // remove duplicates
2390 } else {
2391 $useditems = array();
2394 // MDL-11902
2395 // unset the value if formula is trying to reference to itself
2396 // but array keys does not match itemid
2397 if (!empty($this->id)) {
2398 $useditems = array_diff($useditems, array($this->id));
2399 //unset($useditems[$this->id]);
2402 // prepare formula and init maths library
2403 $formula = preg_replace('/##(gi\d+)##/', '\1', $formulastr);
2404 $formula = new calc_formula($formula);
2407 if (empty($useditems)) {
2408 $grade_items = array();
2410 } else {
2411 list($usql, $params) = $DB->get_in_or_equal($useditems);
2412 $params[] = $this->courseid;
2413 $sql = "SELECT gi.*
2414 FROM {grade_items} gi
2415 WHERE gi.id $usql and gi.courseid=?"; // from the same course only!
2417 if (!$grade_items = $DB->get_records_sql($sql, $params)) {
2418 $grade_items = array();
2422 $params = array();
2423 foreach ($useditems as $itemid) {
2424 // make sure all grade items exist in this course
2425 if (!array_key_exists($itemid, $grade_items)) {
2426 return false;
2428 // use max grade when testing formula, this should be ok in 99.9%
2429 // division by 0 is one of possible problems
2430 $params['gi'.$grade_items[$itemid]->id] = $grade_items[$itemid]->grademax;
2433 // do the calculation
2434 $formula->set_params($params);
2435 $result = $formula->evaluate();
2437 // false as result indicates some problem
2438 if ($result === false) {
2439 // TODO: add more error hints
2440 return get_string('errorcalculationunknown', 'grades');
2441 } else {
2442 return true;
2447 * Returns the value of the display type
2449 * It can be set at 3 levels: grade_item, course setting and site. The lowest level overrides the higher ones.
2451 * @return int Display type
2453 public function get_displaytype() {
2454 global $CFG;
2456 if ($this->display == GRADE_DISPLAY_TYPE_DEFAULT) {
2457 return grade_get_setting($this->courseid, 'displaytype', $CFG->grade_displaytype);
2459 } else {
2460 return $this->display;
2465 * Returns the value of the decimals field
2467 * It can be set at 3 levels: grade_item, course setting and site. The lowest level overrides the higher ones.
2469 * @return int Decimals (0 - 5)
2471 public function get_decimals() {
2472 global $CFG;
2474 if (is_null($this->decimals)) {
2475 return grade_get_setting($this->courseid, 'decimalpoints', $CFG->grade_decimalpoints);
2477 } else {
2478 return $this->decimals;
2483 * Returns a string representing the range of grademin - grademax for this grade item.
2485 * @param int $rangesdisplaytype
2486 * @param int $rangesdecimalpoints
2487 * @return string
2489 function get_formatted_range($rangesdisplaytype=null, $rangesdecimalpoints=null) {
2491 global $USER;
2493 // Determine which display type to use for this average
2494 if (isset($USER->editing) && $USER->editing) {
2495 $displaytype = GRADE_DISPLAY_TYPE_REAL;
2497 } else if ($rangesdisplaytype == GRADE_REPORT_PREFERENCE_INHERIT) { // no ==0 here, please resave report and user prefs
2498 $displaytype = $this->get_displaytype();
2500 } else {
2501 $displaytype = $rangesdisplaytype;
2504 // Override grade_item setting if a display preference (not default) was set for the averages
2505 if ($rangesdecimalpoints == GRADE_REPORT_PREFERENCE_INHERIT) {
2506 $decimalpoints = $this->get_decimals();
2508 } else {
2509 $decimalpoints = $rangesdecimalpoints;
2512 if ($displaytype == GRADE_DISPLAY_TYPE_PERCENTAGE) {
2513 $grademin = "0 %";
2514 $grademax = "100 %";
2516 } else {
2517 $grademin = grade_format_gradevalue($this->grademin, $this, true, $displaytype, $decimalpoints);
2518 $grademax = grade_format_gradevalue($this->grademax, $this, true, $displaytype, $decimalpoints);
2521 return $grademin.'&ndash;'. $grademax;
2525 * Queries parent categories recursively to find the aggregationcoef type that applies to this grade item.
2527 * @return string|false Returns the coefficient string of false is no coefficient is being used
2529 public function get_coefstring() {
2530 $parent_category = $this->load_parent_category();
2531 if ($this->is_category_item()) {
2532 $parent_category = $parent_category->load_parent_category();
2535 if ($parent_category->is_aggregationcoef_used()) {
2536 return $parent_category->get_coefstring();
2537 } else {
2538 return false;
2543 * Returns whether the grade item can control the visibility of the grades
2545 * @return bool
2547 public function can_control_visibility() {
2548 if (core_component::get_plugin_directory($this->itemtype, $this->itemmodule)) {
2549 return !plugin_supports($this->itemtype, $this->itemmodule, FEATURE_CONTROLS_GRADE_VISIBILITY, false);
2551 return parent::can_control_visibility();
2555 * Used to notify the completion system (if necessary) that a user's grade
2556 * has changed, and clear up a possible score cache.
2558 * @param bool $deleted True if grade was actually deleted
2560 protected function notify_changed($deleted) {
2561 global $CFG;
2563 // Condition code may cache the grades for conditional availability of
2564 // modules or sections. (This code should use a hook for communication
2565 // with plugin, but hooks are not implemented at time of writing.)
2566 if (!empty($CFG->enableavailability) && class_exists('\availability_grade\callbacks')) {
2567 \availability_grade\callbacks::grade_item_changed($this->courseid);
2572 * Helper function to get the accurate context for this grade column.
2574 * @return context
2576 public function get_context() {
2577 if ($this->itemtype == 'mod') {
2578 $modinfo = get_fast_modinfo($this->courseid);
2579 // Sometimes the course module cache is out of date and needs to be rebuilt.
2580 if (!isset($modinfo->instances[$this->itemmodule][$this->iteminstance])) {
2581 rebuild_course_cache($this->courseid, true);
2582 $modinfo = get_fast_modinfo($this->courseid);
2584 // Even with a rebuilt cache the module does not exist. This means the
2585 // database is in an invalid state - we will log an error and return
2586 // the course context but the calling code should be updated.
2587 if (!isset($modinfo->instances[$this->itemmodule][$this->iteminstance])) {
2588 mtrace(get_string('moduleinstancedoesnotexist', 'error'));
2589 $context = \context_course::instance($this->courseid);
2590 } else {
2591 $cm = $modinfo->instances[$this->itemmodule][$this->iteminstance];
2592 $context = \context_module::instance($cm->id);
2594 } else {
2595 $context = \context_course::instance($this->courseid);
2597 return $context;