Merge branch 'MDL-44952-30' of git://github.com/marinaglancy/moodle into MOODLE_30_STABLE
[moodle.git] / lib / gradelib.php
blob6dfa2279d753663bae53f73bbbeee1d86d35547b
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 * Library of functions for gradebook - both public and internal
20 * @package core_grades
21 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 defined('MOODLE_INTERNAL') || die();
27 /** Include essential files */
28 require_once($CFG->libdir . '/grade/constants.php');
30 require_once($CFG->libdir . '/grade/grade_category.php');
31 require_once($CFG->libdir . '/grade/grade_item.php');
32 require_once($CFG->libdir . '/grade/grade_grade.php');
33 require_once($CFG->libdir . '/grade/grade_scale.php');
34 require_once($CFG->libdir . '/grade/grade_outcome.php');
36 /////////////////////////////////////////////////////////////////////
37 ///// Start of public API for communication with modules/blocks /////
38 /////////////////////////////////////////////////////////////////////
40 /**
41 * Submit new or update grade; update/create grade_item definition. Grade must have userid specified,
42 * rawgrade and feedback with format are optional. rawgrade NULL means 'Not graded'.
43 * Missing property or key means does not change the existing value.
45 * Only following grade item properties can be changed 'itemname', 'idnumber', 'gradetype', 'grademax',
46 * 'grademin', 'scaleid', 'multfactor', 'plusfactor', 'deleted' and 'hidden'. 'reset' means delete all current grades including locked ones.
48 * Manual, course or category items can not be updated by this function.
50 * @category grade
51 * @param string $source Source of the grade such as 'mod/assignment'
52 * @param int $courseid ID of course
53 * @param string $itemtype Type of grade item. For example, mod or block
54 * @param string $itemmodule More specific then $itemtype. For example, assignment or forum. May be NULL for some item types
55 * @param int $iteminstance Instance ID of graded item
56 * @param int $itemnumber Most probably 0. Modules can use other numbers when having more than one grade for each user
57 * @param mixed $grades Grade (object, array) or several grades (arrays of arrays or objects), NULL if updating grade_item definition only
58 * @param mixed $itemdetails Object or array describing the grading item, NULL if no change
59 * @return int Returns GRADE_UPDATE_OK, GRADE_UPDATE_FAILED, GRADE_UPDATE_MULTIPLE or GRADE_UPDATE_ITEM_LOCKED
61 function grade_update($source, $courseid, $itemtype, $itemmodule, $iteminstance, $itemnumber, $grades=NULL, $itemdetails=NULL) {
62 global $USER, $CFG, $DB;
64 // only following grade_item properties can be changed in this function
65 $allowed = array('itemname', 'idnumber', 'gradetype', 'grademax', 'grademin', 'scaleid', 'multfactor', 'plusfactor', 'deleted', 'hidden');
66 // list of 10,5 numeric fields
67 $floats = array('grademin', 'grademax', 'multfactor', 'plusfactor');
69 // grade item identification
70 $params = compact('courseid', 'itemtype', 'itemmodule', 'iteminstance', 'itemnumber');
72 if (is_null($courseid) or is_null($itemtype)) {
73 debugging('Missing courseid or itemtype');
74 return GRADE_UPDATE_FAILED;
77 if (!$grade_items = grade_item::fetch_all($params)) {
78 // create a new one
79 $grade_item = false;
81 } else if (count($grade_items) == 1){
82 $grade_item = reset($grade_items);
83 unset($grade_items); //release memory
85 } else {
86 debugging('Found more than one grade item');
87 return GRADE_UPDATE_MULTIPLE;
90 if (!empty($itemdetails['deleted'])) {
91 if ($grade_item) {
92 if ($grade_item->delete($source)) {
93 return GRADE_UPDATE_OK;
94 } else {
95 return GRADE_UPDATE_FAILED;
98 return GRADE_UPDATE_OK;
101 /// Create or update the grade_item if needed
103 if (!$grade_item) {
104 if ($itemdetails) {
105 $itemdetails = (array)$itemdetails;
107 // grademin and grademax ignored when scale specified
108 if (array_key_exists('scaleid', $itemdetails)) {
109 if ($itemdetails['scaleid']) {
110 unset($itemdetails['grademin']);
111 unset($itemdetails['grademax']);
115 foreach ($itemdetails as $k=>$v) {
116 if (!in_array($k, $allowed)) {
117 // ignore it
118 continue;
120 if ($k == 'gradetype' and $v == GRADE_TYPE_NONE) {
121 // no grade item needed!
122 return GRADE_UPDATE_OK;
124 $params[$k] = $v;
127 $grade_item = new grade_item($params);
128 $grade_item->insert();
130 } else {
131 if ($grade_item->is_locked()) {
132 // no notice() here, test returned value instead!
133 return GRADE_UPDATE_ITEM_LOCKED;
136 if ($itemdetails) {
137 $itemdetails = (array)$itemdetails;
138 $update = false;
139 foreach ($itemdetails as $k=>$v) {
140 if (!in_array($k, $allowed)) {
141 // ignore it
142 continue;
144 if (in_array($k, $floats)) {
145 if (grade_floats_different($grade_item->{$k}, $v)) {
146 $grade_item->{$k} = $v;
147 $update = true;
150 } else {
151 if ($grade_item->{$k} != $v) {
152 $grade_item->{$k} = $v;
153 $update = true;
157 if ($update) {
158 $grade_item->update();
163 /// reset grades if requested
164 if (!empty($itemdetails['reset'])) {
165 $grade_item->delete_all_grades('reset');
166 return GRADE_UPDATE_OK;
169 /// Some extra checks
170 // do we use grading?
171 if ($grade_item->gradetype == GRADE_TYPE_NONE) {
172 return GRADE_UPDATE_OK;
175 // no grade submitted
176 if (empty($grades)) {
177 return GRADE_UPDATE_OK;
180 /// Finally start processing of grades
181 if (is_object($grades)) {
182 $grades = array($grades->userid=>$grades);
183 } else {
184 if (array_key_exists('userid', $grades)) {
185 $grades = array($grades['userid']=>$grades);
189 /// normalize and verify grade array
190 foreach($grades as $k=>$g) {
191 if (!is_array($g)) {
192 $g = (array)$g;
193 $grades[$k] = $g;
196 if (empty($g['userid']) or $k != $g['userid']) {
197 debugging('Incorrect grade array index, must be user id! Grade ignored.');
198 unset($grades[$k]);
202 if (empty($grades)) {
203 return GRADE_UPDATE_FAILED;
206 $count = count($grades);
207 if ($count > 0 and $count < 200) {
208 list($uids, $params) = $DB->get_in_or_equal(array_keys($grades), SQL_PARAMS_NAMED, $start='uid');
209 $params['gid'] = $grade_item->id;
210 $sql = "SELECT * FROM {grade_grades} WHERE itemid = :gid AND userid $uids";
212 } else {
213 $sql = "SELECT * FROM {grade_grades} WHERE itemid = :gid";
214 $params = array('gid'=>$grade_item->id);
217 $rs = $DB->get_recordset_sql($sql, $params);
219 $failed = false;
221 while (count($grades) > 0) {
222 $grade_grade = null;
223 $grade = null;
225 foreach ($rs as $gd) {
227 $userid = $gd->userid;
228 if (!isset($grades[$userid])) {
229 // this grade not requested, continue
230 continue;
232 // existing grade requested
233 $grade = $grades[$userid];
234 $grade_grade = new grade_grade($gd, false);
235 unset($grades[$userid]);
236 break;
239 if (is_null($grade_grade)) {
240 if (count($grades) == 0) {
241 // no more grades to process
242 break;
245 $grade = reset($grades);
246 $userid = $grade['userid'];
247 $grade_grade = new grade_grade(array('itemid'=>$grade_item->id, 'userid'=>$userid), false);
248 $grade_grade->load_optional_fields(); // add feedback and info too
249 unset($grades[$userid]);
252 $rawgrade = false;
253 $feedback = false;
254 $feedbackformat = FORMAT_MOODLE;
255 $usermodified = $USER->id;
256 $datesubmitted = null;
257 $dategraded = null;
259 if (array_key_exists('rawgrade', $grade)) {
260 $rawgrade = $grade['rawgrade'];
263 if (array_key_exists('feedback', $grade)) {
264 $feedback = $grade['feedback'];
267 if (array_key_exists('feedbackformat', $grade)) {
268 $feedbackformat = $grade['feedbackformat'];
271 if (array_key_exists('usermodified', $grade)) {
272 $usermodified = $grade['usermodified'];
275 if (array_key_exists('datesubmitted', $grade)) {
276 $datesubmitted = $grade['datesubmitted'];
279 if (array_key_exists('dategraded', $grade)) {
280 $dategraded = $grade['dategraded'];
283 // update or insert the grade
284 if (!$grade_item->update_raw_grade($userid, $rawgrade, $source, $feedback, $feedbackformat, $usermodified, $dategraded, $datesubmitted, $grade_grade)) {
285 $failed = true;
289 if ($rs) {
290 $rs->close();
293 if (!$failed) {
294 return GRADE_UPDATE_OK;
295 } else {
296 return GRADE_UPDATE_FAILED;
301 * Updates a user's outcomes. Manual outcomes can not be updated.
303 * @category grade
304 * @param string $source Source of the grade such as 'mod/assignment'
305 * @param int $courseid ID of course
306 * @param string $itemtype Type of grade item. For example, 'mod' or 'block'
307 * @param string $itemmodule More specific then $itemtype. For example, 'forum' or 'quiz'. May be NULL for some item types
308 * @param int $iteminstance Instance ID of graded item. For example the forum ID.
309 * @param int $userid ID of the graded user
310 * @param array $data Array consisting of grade item itemnumber ({@link grade_update()}) => outcomegrade
311 * @return bool returns true if grade items were found and updated successfully
313 function grade_update_outcomes($source, $courseid, $itemtype, $itemmodule, $iteminstance, $userid, $data) {
314 if ($items = grade_item::fetch_all(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid))) {
315 $result = true;
316 foreach ($items as $item) {
317 if (!array_key_exists($item->itemnumber, $data)) {
318 continue;
320 $grade = $data[$item->itemnumber] < 1 ? null : $data[$item->itemnumber];
321 $result = ($item->update_final_grade($userid, $grade, $source) && $result);
323 return $result;
325 return false; //grade items not found
329 * Returns grading information for given activity, optionally with user grades
330 * Manual, course or category items can not be queried.
332 * @category grade
333 * @param int $courseid ID of course
334 * @param string $itemtype Type of grade item. For example, 'mod' or 'block'
335 * @param string $itemmodule More specific then $itemtype. For example, 'forum' or 'quiz'. May be NULL for some item types
336 * @param int $iteminstance ID of the item module
337 * @param mixed $userid_or_ids Either a single user ID, an array of user IDs or null. If user ID or IDs are not supplied returns information about grade_item
338 * @return array Array of grade information objects (scaleid, name, grade and locked status, etc.) indexed with itemnumbers
340 function grade_get_grades($courseid, $itemtype, $itemmodule, $iteminstance, $userid_or_ids=null) {
341 global $CFG;
343 $return = new stdClass();
344 $return->items = array();
345 $return->outcomes = array();
347 $course_item = grade_item::fetch_course_item($courseid);
348 $needsupdate = array();
349 if ($course_item->needsupdate) {
350 $result = grade_regrade_final_grades($courseid);
351 if ($result !== true) {
352 $needsupdate = array_keys($result);
356 if ($grade_items = grade_item::fetch_all(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid))) {
357 foreach ($grade_items as $grade_item) {
358 $decimalpoints = null;
360 if (empty($grade_item->outcomeid)) {
361 // prepare information about grade item
362 $item = new stdClass();
363 $item->id = $grade_item->id;
364 $item->itemnumber = $grade_item->itemnumber;
365 $item->itemtype = $grade_item->itemtype;
366 $item->itemmodule = $grade_item->itemmodule;
367 $item->iteminstance = $grade_item->iteminstance;
368 $item->scaleid = $grade_item->scaleid;
369 $item->name = $grade_item->get_name();
370 $item->grademin = $grade_item->grademin;
371 $item->grademax = $grade_item->grademax;
372 $item->gradepass = $grade_item->gradepass;
373 $item->locked = $grade_item->is_locked();
374 $item->hidden = $grade_item->is_hidden();
375 $item->grades = array();
377 switch ($grade_item->gradetype) {
378 case GRADE_TYPE_NONE:
379 continue;
381 case GRADE_TYPE_VALUE:
382 $item->scaleid = 0;
383 break;
385 case GRADE_TYPE_TEXT:
386 $item->scaleid = 0;
387 $item->grademin = 0;
388 $item->grademax = 0;
389 $item->gradepass = 0;
390 break;
393 if (empty($userid_or_ids)) {
394 $userids = array();
396 } else if (is_array($userid_or_ids)) {
397 $userids = $userid_or_ids;
399 } else {
400 $userids = array($userid_or_ids);
403 if ($userids) {
404 $grade_grades = grade_grade::fetch_users_grades($grade_item, $userids, true);
405 foreach ($userids as $userid) {
406 $grade_grades[$userid]->grade_item =& $grade_item;
408 $grade = new stdClass();
409 $grade->grade = $grade_grades[$userid]->finalgrade;
410 $grade->locked = $grade_grades[$userid]->is_locked();
411 $grade->hidden = $grade_grades[$userid]->is_hidden();
412 $grade->overridden = $grade_grades[$userid]->overridden;
413 $grade->feedback = $grade_grades[$userid]->feedback;
414 $grade->feedbackformat = $grade_grades[$userid]->feedbackformat;
415 $grade->usermodified = $grade_grades[$userid]->usermodified;
416 $grade->datesubmitted = $grade_grades[$userid]->get_datesubmitted();
417 $grade->dategraded = $grade_grades[$userid]->get_dategraded();
419 // create text representation of grade
420 if ($grade_item->gradetype == GRADE_TYPE_TEXT or $grade_item->gradetype == GRADE_TYPE_NONE) {
421 $grade->grade = null;
422 $grade->str_grade = '-';
423 $grade->str_long_grade = $grade->str_grade;
425 } else if (in_array($grade_item->id, $needsupdate)) {
426 $grade->grade = false;
427 $grade->str_grade = get_string('error');
428 $grade->str_long_grade = $grade->str_grade;
430 } else if (is_null($grade->grade)) {
431 $grade->str_grade = '-';
432 $grade->str_long_grade = $grade->str_grade;
434 } else {
435 $grade->str_grade = grade_format_gradevalue($grade->grade, $grade_item);
436 if ($grade_item->gradetype == GRADE_TYPE_SCALE or $grade_item->get_displaytype() != GRADE_DISPLAY_TYPE_REAL) {
437 $grade->str_long_grade = $grade->str_grade;
438 } else {
439 $a = new stdClass();
440 $a->grade = $grade->str_grade;
441 $a->max = grade_format_gradevalue($grade_item->grademax, $grade_item);
442 $grade->str_long_grade = get_string('gradelong', 'grades', $a);
446 // create html representation of feedback
447 if (is_null($grade->feedback)) {
448 $grade->str_feedback = '';
449 } else {
450 $grade->str_feedback = format_text($grade->feedback, $grade->feedbackformat);
453 $item->grades[$userid] = $grade;
456 $return->items[$grade_item->itemnumber] = $item;
458 } else {
459 if (!$grade_outcome = grade_outcome::fetch(array('id'=>$grade_item->outcomeid))) {
460 debugging('Incorect outcomeid found');
461 continue;
464 // outcome info
465 $outcome = new stdClass();
466 $outcome->id = $grade_item->id;
467 $outcome->itemnumber = $grade_item->itemnumber;
468 $outcome->itemtype = $grade_item->itemtype;
469 $outcome->itemmodule = $grade_item->itemmodule;
470 $outcome->iteminstance = $grade_item->iteminstance;
471 $outcome->scaleid = $grade_outcome->scaleid;
472 $outcome->name = $grade_outcome->get_name();
473 $outcome->locked = $grade_item->is_locked();
474 $outcome->hidden = $grade_item->is_hidden();
476 if (empty($userid_or_ids)) {
477 $userids = array();
478 } else if (is_array($userid_or_ids)) {
479 $userids = $userid_or_ids;
480 } else {
481 $userids = array($userid_or_ids);
484 if ($userids) {
485 $grade_grades = grade_grade::fetch_users_grades($grade_item, $userids, true);
486 foreach ($userids as $userid) {
487 $grade_grades[$userid]->grade_item =& $grade_item;
489 $grade = new stdClass();
490 $grade->grade = $grade_grades[$userid]->finalgrade;
491 $grade->locked = $grade_grades[$userid]->is_locked();
492 $grade->hidden = $grade_grades[$userid]->is_hidden();
493 $grade->feedback = $grade_grades[$userid]->feedback;
494 $grade->feedbackformat = $grade_grades[$userid]->feedbackformat;
495 $grade->usermodified = $grade_grades[$userid]->usermodified;
497 // create text representation of grade
498 if (in_array($grade_item->id, $needsupdate)) {
499 $grade->grade = false;
500 $grade->str_grade = get_string('error');
502 } else if (is_null($grade->grade)) {
503 $grade->grade = 0;
504 $grade->str_grade = get_string('nooutcome', 'grades');
506 } else {
507 $grade->grade = (int)$grade->grade;
508 $scale = $grade_item->load_scale();
509 $grade->str_grade = format_string($scale->scale_items[(int)$grade->grade-1]);
512 // create html representation of feedback
513 if (is_null($grade->feedback)) {
514 $grade->str_feedback = '';
515 } else {
516 $grade->str_feedback = format_text($grade->feedback, $grade->feedbackformat);
519 $outcome->grades[$userid] = $grade;
523 if (isset($return->outcomes[$grade_item->itemnumber])) {
524 // itemnumber duplicates - lets fix them!
525 $newnumber = $grade_item->itemnumber + 1;
526 while(grade_item::fetch(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid, 'itemnumber'=>$newnumber))) {
527 $newnumber++;
529 $outcome->itemnumber = $newnumber;
530 $grade_item->itemnumber = $newnumber;
531 $grade_item->update('system');
534 $return->outcomes[$grade_item->itemnumber] = $outcome;
540 // sort results using itemnumbers
541 ksort($return->items, SORT_NUMERIC);
542 ksort($return->outcomes, SORT_NUMERIC);
544 return $return;
547 ///////////////////////////////////////////////////////////////////
548 ///// End of public API for communication with modules/blocks /////
549 ///////////////////////////////////////////////////////////////////
553 ///////////////////////////////////////////////////////////////////
554 ///// Internal API: used by gradebook plugins and Moodle core /////
555 ///////////////////////////////////////////////////////////////////
558 * Returns a course gradebook setting
560 * @param int $courseid
561 * @param string $name of setting, maybe null if reset only
562 * @param string $default value to return if setting is not found
563 * @param bool $resetcache force reset of internal static cache
564 * @return string value of the setting, $default if setting not found, NULL if supplied $name is null
566 function grade_get_setting($courseid, $name, $default=null, $resetcache=false) {
567 global $DB;
569 static $cache = array();
571 if ($resetcache or !array_key_exists($courseid, $cache)) {
572 $cache[$courseid] = array();
574 } else if (is_null($name)) {
575 return null;
577 } else if (array_key_exists($name, $cache[$courseid])) {
578 return $cache[$courseid][$name];
581 if (!$data = $DB->get_record('grade_settings', array('courseid'=>$courseid, 'name'=>$name))) {
582 $result = null;
583 } else {
584 $result = $data->value;
587 if (is_null($result)) {
588 $result = $default;
591 $cache[$courseid][$name] = $result;
592 return $result;
596 * Returns all course gradebook settings as object properties
598 * @param int $courseid
599 * @return object
601 function grade_get_settings($courseid) {
602 global $DB;
604 $settings = new stdClass();
605 $settings->id = $courseid;
607 if ($records = $DB->get_records('grade_settings', array('courseid'=>$courseid))) {
608 foreach ($records as $record) {
609 $settings->{$record->name} = $record->value;
613 return $settings;
617 * Add, update or delete a course gradebook setting
619 * @param int $courseid The course ID
620 * @param string $name Name of the setting
621 * @param string $value Value of the setting. NULL means delete the setting.
623 function grade_set_setting($courseid, $name, $value) {
624 global $DB;
626 if (is_null($value)) {
627 $DB->delete_records('grade_settings', array('courseid'=>$courseid, 'name'=>$name));
629 } else if (!$existing = $DB->get_record('grade_settings', array('courseid'=>$courseid, 'name'=>$name))) {
630 $data = new stdClass();
631 $data->courseid = $courseid;
632 $data->name = $name;
633 $data->value = $value;
634 $DB->insert_record('grade_settings', $data);
636 } else {
637 $data = new stdClass();
638 $data->id = $existing->id;
639 $data->value = $value;
640 $DB->update_record('grade_settings', $data);
643 grade_get_setting($courseid, null, null, true); // reset the cache
647 * Returns string representation of grade value
649 * @param float $value The grade value
650 * @param object $grade_item Grade item object passed by reference to prevent scale reloading
651 * @param bool $localized use localised decimal separator
652 * @param int $displaytype type of display. For example GRADE_DISPLAY_TYPE_REAL, GRADE_DISPLAY_TYPE_PERCENTAGE, GRADE_DISPLAY_TYPE_LETTER
653 * @param int $decimals The number of decimal places when displaying float values
654 * @return string
656 function grade_format_gradevalue($value, &$grade_item, $localized=true, $displaytype=null, $decimals=null) {
657 if ($grade_item->gradetype == GRADE_TYPE_NONE or $grade_item->gradetype == GRADE_TYPE_TEXT) {
658 return '';
661 // no grade yet?
662 if (is_null($value)) {
663 return '-';
666 if ($grade_item->gradetype != GRADE_TYPE_VALUE and $grade_item->gradetype != GRADE_TYPE_SCALE) {
667 //unknown type??
668 return '';
671 if (is_null($displaytype)) {
672 $displaytype = $grade_item->get_displaytype();
675 if (is_null($decimals)) {
676 $decimals = $grade_item->get_decimals();
679 switch ($displaytype) {
680 case GRADE_DISPLAY_TYPE_REAL:
681 return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized);
683 case GRADE_DISPLAY_TYPE_PERCENTAGE:
684 return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized);
686 case GRADE_DISPLAY_TYPE_LETTER:
687 return grade_format_gradevalue_letter($value, $grade_item);
689 case GRADE_DISPLAY_TYPE_REAL_PERCENTAGE:
690 return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ' (' .
691 grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ')';
693 case GRADE_DISPLAY_TYPE_REAL_LETTER:
694 return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ' (' .
695 grade_format_gradevalue_letter($value, $grade_item) . ')';
697 case GRADE_DISPLAY_TYPE_PERCENTAGE_REAL:
698 return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ' (' .
699 grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ')';
701 case GRADE_DISPLAY_TYPE_LETTER_REAL:
702 return grade_format_gradevalue_letter($value, $grade_item) . ' (' .
703 grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ')';
705 case GRADE_DISPLAY_TYPE_LETTER_PERCENTAGE:
706 return grade_format_gradevalue_letter($value, $grade_item) . ' (' .
707 grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ')';
709 case GRADE_DISPLAY_TYPE_PERCENTAGE_LETTER:
710 return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ' (' .
711 grade_format_gradevalue_letter($value, $grade_item) . ')';
712 default:
713 return '';
718 * Returns a float representation of a grade value
720 * @param float $value The grade value
721 * @param object $grade_item Grade item object
722 * @param int $decimals The number of decimal places
723 * @param bool $localized use localised decimal separator
724 * @return string
726 function grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) {
727 if ($grade_item->gradetype == GRADE_TYPE_SCALE) {
728 if (!$scale = $grade_item->load_scale()) {
729 return get_string('error');
732 $value = $grade_item->bounded_grade($value);
733 return format_string($scale->scale_items[$value-1]);
735 } else {
736 return format_float($value, $decimals, $localized);
741 * Returns a percentage representation of a grade value
743 * @param float $value The grade value
744 * @param object $grade_item Grade item object
745 * @param int $decimals The number of decimal places
746 * @param bool $localized use localised decimal separator
747 * @return string
749 function grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) {
750 $min = $grade_item->grademin;
751 $max = $grade_item->grademax;
752 if ($min == $max) {
753 return '';
755 $value = $grade_item->bounded_grade($value);
756 $percentage = (($value-$min)*100)/($max-$min);
757 return format_float($percentage, $decimals, $localized).' %';
761 * Returns a letter grade representation of a grade value
762 * The array of grade letters used is produced by {@link grade_get_letters()} using the course context
764 * @param float $value The grade value
765 * @param object $grade_item Grade item object
766 * @return string
768 function grade_format_gradevalue_letter($value, $grade_item) {
769 $context = context_course::instance($grade_item->courseid, IGNORE_MISSING);
770 if (!$letters = grade_get_letters($context)) {
771 return ''; // no letters??
774 if (is_null($value)) {
775 return '-';
778 $value = grade_grade::standardise_score($value, $grade_item->grademin, $grade_item->grademax, 0, 100);
779 $value = bounded_number(0, $value, 100); // just in case
780 foreach ($letters as $boundary => $letter) {
781 if ($value >= $boundary) {
782 return format_string($letter);
785 return '-'; // no match? maybe '' would be more correct
790 * Returns grade options for gradebook grade category menu
792 * @param int $courseid The course ID
793 * @param bool $includenew Include option for new category at array index -1
794 * @return array of grade categories in course
796 function grade_get_categories_menu($courseid, $includenew=false) {
797 $result = array();
798 if (!$categories = grade_category::fetch_all(array('courseid'=>$courseid))) {
799 //make sure course category exists
800 if (!grade_category::fetch_course_category($courseid)) {
801 debugging('Can not create course grade category!');
802 return $result;
804 $categories = grade_category::fetch_all(array('courseid'=>$courseid));
806 foreach ($categories as $key=>$category) {
807 if ($category->is_course_category()) {
808 $result[$category->id] = get_string('uncategorised', 'grades');
809 unset($categories[$key]);
812 if ($includenew) {
813 $result[-1] = get_string('newcategory', 'grades');
815 $cats = array();
816 foreach ($categories as $category) {
817 $cats[$category->id] = $category->get_name();
819 core_collator::asort($cats);
821 return ($result+$cats);
825 * Returns the array of grade letters to be used in the supplied context
827 * @param object $context Context object or null for defaults
828 * @return array of grade_boundary (minimum) => letter_string
830 function grade_get_letters($context=null) {
831 global $DB;
833 if (empty($context)) {
834 //default grading letters
835 return array('93'=>'A', '90'=>'A-', '87'=>'B+', '83'=>'B', '80'=>'B-', '77'=>'C+', '73'=>'C', '70'=>'C-', '67'=>'D+', '60'=>'D', '0'=>'F');
838 static $cache = array();
840 if (array_key_exists($context->id, $cache)) {
841 return $cache[$context->id];
844 if (count($cache) > 100) {
845 $cache = array(); // cache size limit
848 $letters = array();
850 $contexts = $context->get_parent_context_ids();
851 array_unshift($contexts, $context->id);
853 foreach ($contexts as $ctxid) {
854 if ($records = $DB->get_records('grade_letters', array('contextid'=>$ctxid), 'lowerboundary DESC')) {
855 foreach ($records as $record) {
856 $letters[$record->lowerboundary] = $record->letter;
860 if (!empty($letters)) {
861 $cache[$context->id] = $letters;
862 return $letters;
866 $letters = grade_get_letters(null);
867 $cache[$context->id] = $letters;
868 return $letters;
873 * Verify new value of grade item idnumber. Checks for uniqueness of new ID numbers. Old ID numbers are kept intact.
875 * @param string $idnumber string (with magic quotes)
876 * @param int $courseid ID numbers are course unique only
877 * @param grade_item $grade_item The grade item this idnumber is associated with
878 * @param stdClass $cm used for course module idnumbers and items attached to modules
879 * @return bool true means idnumber ok
881 function grade_verify_idnumber($idnumber, $courseid, $grade_item=null, $cm=null) {
882 global $DB;
884 if ($idnumber == '') {
885 //we allow empty idnumbers
886 return true;
889 // keep existing even when not unique
890 if ($cm and $cm->idnumber == $idnumber) {
891 if ($grade_item and $grade_item->itemnumber != 0) {
892 // grade item with itemnumber > 0 can't have the same idnumber as the main
893 // itemnumber 0 which is synced with course_modules
894 return false;
896 return true;
897 } else if ($grade_item and $grade_item->idnumber == $idnumber) {
898 return true;
901 if ($DB->record_exists('course_modules', array('course'=>$courseid, 'idnumber'=>$idnumber))) {
902 return false;
905 if ($DB->record_exists('grade_items', array('courseid'=>$courseid, 'idnumber'=>$idnumber))) {
906 return false;
909 return true;
913 * Force final grade recalculation in all course items
915 * @param int $courseid The course ID to recalculate
917 function grade_force_full_regrading($courseid) {
918 global $DB;
919 $DB->set_field('grade_items', 'needsupdate', 1, array('courseid'=>$courseid));
923 * Forces regrading of all site grades. Used when changing site setings
925 function grade_force_site_regrading() {
926 global $CFG, $DB;
927 $DB->set_field('grade_items', 'needsupdate', 1);
931 * Recover a user's grades from grade_grades_history
932 * @param int $userid the user ID whose grades we want to recover
933 * @param int $courseid the relevant course
934 * @return bool true if successful or false if there was an error or no grades could be recovered
936 function grade_recover_history_grades($userid, $courseid) {
937 global $CFG, $DB;
939 if ($CFG->disablegradehistory) {
940 debugging('Attempting to recover grades when grade history is disabled.');
941 return false;
944 //Were grades recovered? Flag to return.
945 $recoveredgrades = false;
947 //Check the user is enrolled in this course
948 //Dont bother checking if they have a gradeable role. They may get one later so recover
949 //whatever grades they have now just in case.
950 $course_context = context_course::instance($courseid);
951 if (!is_enrolled($course_context, $userid)) {
952 debugging('Attempting to recover the grades of a user who is deleted or not enrolled. Skipping recover.');
953 return false;
956 //Check for existing grades for this user in this course
957 //Recovering grades when the user already has grades can lead to duplicate indexes and bad data
958 //In the future we could move the existing grades to the history table then recover the grades from before then
959 $sql = "SELECT gg.id
960 FROM {grade_grades} gg
961 JOIN {grade_items} gi ON gi.id = gg.itemid
962 WHERE gi.courseid = :courseid AND gg.userid = :userid";
963 $params = array('userid' => $userid, 'courseid' => $courseid);
964 if ($DB->record_exists_sql($sql, $params)) {
965 debugging('Attempting to recover the grades of a user who already has grades. Skipping recover.');
966 return false;
967 } else {
968 //Retrieve the user's old grades
969 //have history ID as first column to guarantee we a unique first column
970 $sql = "SELECT h.id, gi.itemtype, gi.itemmodule, gi.iteminstance as iteminstance, gi.itemnumber, h.source, h.itemid, h.userid, h.rawgrade, h.rawgrademax,
971 h.rawgrademin, h.rawscaleid, h.usermodified, h.finalgrade, h.hidden, h.locked, h.locktime, h.exported, h.overridden, h.excluded, h.feedback,
972 h.feedbackformat, h.information, h.informationformat, h.timemodified, itemcreated.tm AS timecreated
973 FROM {grade_grades_history} h
974 JOIN (SELECT itemid, MAX(id) AS id
975 FROM {grade_grades_history}
976 WHERE userid = :userid1
977 GROUP BY itemid) maxquery ON h.id = maxquery.id AND h.itemid = maxquery.itemid
978 JOIN {grade_items} gi ON gi.id = h.itemid
979 JOIN (SELECT itemid, MAX(timemodified) AS tm
980 FROM {grade_grades_history}
981 WHERE userid = :userid2 AND action = :insertaction
982 GROUP BY itemid) itemcreated ON itemcreated.itemid = h.itemid
983 WHERE gi.courseid = :courseid";
984 $params = array('userid1' => $userid, 'userid2' => $userid , 'insertaction' => GRADE_HISTORY_INSERT, 'courseid' => $courseid);
985 $oldgrades = $DB->get_records_sql($sql, $params);
987 //now move the old grades to the grade_grades table
988 foreach ($oldgrades as $oldgrade) {
989 unset($oldgrade->id);
991 $grade = new grade_grade($oldgrade, false);//2nd arg false as dont want to try and retrieve a record from the DB
992 $grade->insert($oldgrade->source);
994 //dont include default empty grades created when activities are created
995 if (!is_null($oldgrade->finalgrade) || !is_null($oldgrade->feedback)) {
996 $recoveredgrades = true;
1001 //Some activities require manual grade synching (moving grades from the activity into the gradebook)
1002 //If the student was deleted when synching was done they may have grades in the activity that haven't been moved across
1003 grade_grab_course_grades($courseid, null, $userid);
1005 return $recoveredgrades;
1009 * Updates all final grades in course.
1011 * @param int $courseid The course ID
1012 * @param int $userid If specified try to do a quick regrading of the grades of this user only
1013 * @param object $updated_item Optional grade item to be marked for regrading
1014 * @return bool true if ok, array of errors if problems found. Grade item id => error message
1016 function grade_regrade_final_grades($courseid, $userid=null, $updated_item=null) {
1017 // This may take a very long time.
1018 \core_php_time_limit::raise();
1020 $course_item = grade_item::fetch_course_item($courseid);
1022 if ($userid) {
1023 // one raw grade updated for one user
1024 if (empty($updated_item)) {
1025 print_error("cannotbenull", 'debug', '', "updated_item");
1027 if ($course_item->needsupdate) {
1028 $updated_item->force_regrading();
1029 return array($course_item->id =>'Can not do fast regrading after updating of raw grades');
1032 } else {
1033 if (!$course_item->needsupdate) {
1034 // nothing to do :-)
1035 return true;
1039 // Categories might have to run some processing before we fetch the grade items.
1040 // This gives them a final opportunity to update and mark their children to be updated.
1041 // We need to work on the children categories up to the parent ones, so that, for instance,
1042 // if a category total is updated it will be reflected in the parent category.
1043 $cats = grade_category::fetch_all(array('courseid' => $courseid));
1044 $flatcattree = array();
1045 foreach ($cats as $cat) {
1046 if (!isset($flatcattree[$cat->depth])) {
1047 $flatcattree[$cat->depth] = array();
1049 $flatcattree[$cat->depth][] = $cat;
1051 krsort($flatcattree);
1052 foreach ($flatcattree as $depth => $cats) {
1053 foreach ($cats as $cat) {
1054 $cat->pre_regrade_final_grades();
1058 $grade_items = grade_item::fetch_all(array('courseid'=>$courseid));
1059 $depends_on = array();
1061 // first mark all category and calculated items as needing regrading
1062 // this is slower, but 100% accurate
1063 foreach ($grade_items as $gid=>$gitem) {
1064 if (!empty($updated_item) and $updated_item->id == $gid) {
1065 $grade_items[$gid]->needsupdate = 1;
1067 } else if ($gitem->is_course_item() or $gitem->is_category_item() or $gitem->is_calculated()) {
1068 $grade_items[$gid]->needsupdate = 1;
1071 // construct depends_on lookup array
1072 $depends_on[$gid] = $grade_items[$gid]->depends_on();
1075 $errors = array();
1076 $finalids = array();
1077 $gids = array_keys($grade_items);
1078 $failed = 0;
1080 while (count($finalids) < count($gids)) { // work until all grades are final or error found
1081 $count = 0;
1082 foreach ($gids as $gid) {
1083 if (in_array($gid, $finalids)) {
1084 continue; // already final
1087 if (!$grade_items[$gid]->needsupdate) {
1088 $finalids[] = $gid; // we can make it final - does not need update
1089 continue;
1092 $doupdate = true;
1093 foreach ($depends_on[$gid] as $did) {
1094 if (!in_array($did, $finalids)) {
1095 $doupdate = false;
1096 continue; // this item depends on something that is not yet in finals array
1100 //oki - let's update, calculate or aggregate :-)
1101 if ($doupdate) {
1102 $result = $grade_items[$gid]->regrade_final_grades($userid);
1104 if ($result === true) {
1105 $grade_items[$gid]->regrading_finished();
1106 $grade_items[$gid]->check_locktime(); // do the locktime item locking
1107 $count++;
1108 $finalids[] = $gid;
1110 } else {
1111 $grade_items[$gid]->force_regrading();
1112 $errors[$gid] = $result;
1117 if ($count == 0) {
1118 $failed++;
1119 } else {
1120 $failed = 0;
1123 if ($failed > 1) {
1124 foreach($gids as $gid) {
1125 if (in_array($gid, $finalids)) {
1126 continue; // this one is ok
1128 $grade_items[$gid]->force_regrading();
1129 $errors[$grade_items[$gid]->id] = get_string('errorcalculationbroken', 'grades');
1131 break; // Found error.
1135 if (count($errors) == 0) {
1136 if (empty($userid)) {
1137 // do the locktime locking of grades, but only when doing full regrading
1138 grade_grade::check_locktime_all($gids);
1140 return true;
1141 } else {
1142 return $errors;
1147 * Refetches grade data from course activities
1149 * @param int $courseid The course ID
1150 * @param string $modname Limit the grade fetch to a single module type. For example 'forum'
1151 * @param int $userid limit the grade fetch to a single user
1153 function grade_grab_course_grades($courseid, $modname=null, $userid=0) {
1154 global $CFG, $DB;
1156 if ($modname) {
1157 $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname
1158 FROM {".$modname."} a, {course_modules} cm, {modules} m
1159 WHERE m.name=:modname AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=:courseid";
1160 $params = array('modname'=>$modname, 'courseid'=>$courseid);
1162 if ($modinstances = $DB->get_records_sql($sql, $params)) {
1163 foreach ($modinstances as $modinstance) {
1164 grade_update_mod_grades($modinstance, $userid);
1167 return;
1170 if (!$mods = core_component::get_plugin_list('mod') ) {
1171 print_error('nomodules', 'debug');
1174 foreach ($mods as $mod => $fullmod) {
1175 if ($mod == 'NEWMODULE') { // Someone has unzipped the template, ignore it
1176 continue;
1179 // include the module lib once
1180 if (file_exists($fullmod.'/lib.php')) {
1181 // get all instance of the activity
1182 $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname
1183 FROM {".$mod."} a, {course_modules} cm, {modules} m
1184 WHERE m.name=:mod AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=:courseid";
1185 $params = array('mod'=>$mod, 'courseid'=>$courseid);
1187 if ($modinstances = $DB->get_records_sql($sql, $params)) {
1188 foreach ($modinstances as $modinstance) {
1189 grade_update_mod_grades($modinstance, $userid);
1197 * Force full update of module grades in central gradebook
1199 * @param object $modinstance Module object with extra cmidnumber and modname property
1200 * @param int $userid Optional user ID if limiting the update to a single user
1201 * @return bool True if success
1203 function grade_update_mod_grades($modinstance, $userid=0) {
1204 global $CFG, $DB;
1206 $fullmod = $CFG->dirroot.'/mod/'.$modinstance->modname;
1207 if (!file_exists($fullmod.'/lib.php')) {
1208 debugging('missing lib.php file in module ' . $modinstance->modname);
1209 return false;
1211 include_once($fullmod.'/lib.php');
1213 $updateitemfunc = $modinstance->modname.'_grade_item_update';
1214 $updategradesfunc = $modinstance->modname.'_update_grades';
1216 if (function_exists($updategradesfunc) and function_exists($updateitemfunc)) {
1217 //new grading supported, force updating of grades
1218 $updateitemfunc($modinstance);
1219 $updategradesfunc($modinstance, $userid);
1221 } else {
1222 // Module does not support grading?
1225 return true;
1229 * Remove grade letters for given context
1231 * @param context $context The context
1232 * @param bool $showfeedback If true a success notification will be displayed
1234 function remove_grade_letters($context, $showfeedback) {
1235 global $DB, $OUTPUT;
1237 $strdeleted = get_string('deleted');
1239 $DB->delete_records('grade_letters', array('contextid'=>$context->id));
1240 if ($showfeedback) {
1241 echo $OUTPUT->notification($strdeleted.' - '.get_string('letters', 'grades'), 'notifysuccess');
1246 * Remove all grade related course data
1247 * Grade history is kept
1249 * @param int $courseid The course ID
1250 * @param bool $showfeedback If true success notifications will be displayed
1252 function remove_course_grades($courseid, $showfeedback) {
1253 global $DB, $OUTPUT;
1255 $fs = get_file_storage();
1256 $strdeleted = get_string('deleted');
1258 $course_category = grade_category::fetch_course_category($courseid);
1259 $course_category->delete('coursedelete');
1260 $fs->delete_area_files(context_course::instance($courseid)->id, 'grade', 'feedback');
1261 if ($showfeedback) {
1262 echo $OUTPUT->notification($strdeleted.' - '.get_string('grades', 'grades').', '.get_string('items', 'grades').', '.get_string('categories', 'grades'), 'notifysuccess');
1265 if ($outcomes = grade_outcome::fetch_all(array('courseid'=>$courseid))) {
1266 foreach ($outcomes as $outcome) {
1267 $outcome->delete('coursedelete');
1270 $DB->delete_records('grade_outcomes_courses', array('courseid'=>$courseid));
1271 if ($showfeedback) {
1272 echo $OUTPUT->notification($strdeleted.' - '.get_string('outcomes', 'grades'), 'notifysuccess');
1275 if ($scales = grade_scale::fetch_all(array('courseid'=>$courseid))) {
1276 foreach ($scales as $scale) {
1277 $scale->delete('coursedelete');
1280 if ($showfeedback) {
1281 echo $OUTPUT->notification($strdeleted.' - '.get_string('scales'), 'notifysuccess');
1284 $DB->delete_records('grade_settings', array('courseid'=>$courseid));
1285 if ($showfeedback) {
1286 echo $OUTPUT->notification($strdeleted.' - '.get_string('settings', 'grades'), 'notifysuccess');
1291 * Called when course category is deleted
1292 * Cleans the gradebook of associated data
1294 * @param int $categoryid The course category id
1295 * @param int $newparentid If empty everything is deleted. Otherwise the ID of the category where content moved
1296 * @param bool $showfeedback print feedback
1298 function grade_course_category_delete($categoryid, $newparentid, $showfeedback) {
1299 global $DB;
1301 $context = context_coursecat::instance($categoryid);
1302 $DB->delete_records('grade_letters', array('contextid'=>$context->id));
1306 * Does gradebook cleanup when a module is uninstalled
1307 * Deletes all associated grade items
1309 * @param string $modname The grade item module name to remove. For example 'forum'
1311 function grade_uninstalled_module($modname) {
1312 global $CFG, $DB;
1314 $sql = "SELECT *
1315 FROM {grade_items}
1316 WHERE itemtype='mod' AND itemmodule=?";
1318 // go all items for this module and delete them including the grades
1319 $rs = $DB->get_recordset_sql($sql, array($modname));
1320 foreach ($rs as $item) {
1321 $grade_item = new grade_item($item, false);
1322 $grade_item->delete('moduninstall');
1324 $rs->close();
1328 * Deletes all of a user's grade data from gradebook
1330 * @param int $userid The user whose grade data should be deleted
1332 function grade_user_delete($userid) {
1333 if ($grades = grade_grade::fetch_all(array('userid'=>$userid))) {
1334 foreach ($grades as $grade) {
1335 $grade->delete('userdelete');
1341 * Purge course data when user unenrolls from a course
1343 * @param int $courseid The ID of the course the user has unenrolled from
1344 * @param int $userid The ID of the user unenrolling
1346 function grade_user_unenrol($courseid, $userid) {
1347 if ($items = grade_item::fetch_all(array('courseid'=>$courseid))) {
1348 foreach ($items as $item) {
1349 if ($grades = grade_grade::fetch_all(array('userid'=>$userid, 'itemid'=>$item->id))) {
1350 foreach ($grades as $grade) {
1351 $grade->delete('userdelete');
1359 * Grading cron job. Performs background clean up on the gradebook
1361 function grade_cron() {
1362 global $CFG, $DB;
1364 $now = time();
1366 $sql = "SELECT i.*
1367 FROM {grade_items} i
1368 WHERE i.locked = 0 AND i.locktime > 0 AND i.locktime < ? AND EXISTS (
1369 SELECT 'x' FROM {grade_items} c WHERE c.itemtype='course' AND c.needsupdate=0 AND c.courseid=i.courseid)";
1371 // go through all courses that have proper final grades and lock them if needed
1372 $rs = $DB->get_recordset_sql($sql, array($now));
1373 foreach ($rs as $item) {
1374 $grade_item = new grade_item($item, false);
1375 $grade_item->locked = $now;
1376 $grade_item->update('locktime');
1378 $rs->close();
1380 $grade_inst = new grade_grade();
1381 $fields = 'g.'.implode(',g.', $grade_inst->required_fields);
1383 $sql = "SELECT $fields
1384 FROM {grade_grades} g, {grade_items} i
1385 WHERE g.locked = 0 AND g.locktime > 0 AND g.locktime < ? AND g.itemid=i.id AND EXISTS (
1386 SELECT 'x' FROM {grade_items} c WHERE c.itemtype='course' AND c.needsupdate=0 AND c.courseid=i.courseid)";
1388 // go through all courses that have proper final grades and lock them if needed
1389 $rs = $DB->get_recordset_sql($sql, array($now));
1390 foreach ($rs as $grade) {
1391 $grade_grade = new grade_grade($grade, false);
1392 $grade_grade->locked = $now;
1393 $grade_grade->update('locktime');
1395 $rs->close();
1397 //TODO: do not run this cleanup every cron invocation
1398 // cleanup history tables
1399 if (!empty($CFG->gradehistorylifetime)) { // value in days
1400 $histlifetime = $now - ($CFG->gradehistorylifetime * 3600 * 24);
1401 $tables = array('grade_outcomes_history', 'grade_categories_history', 'grade_items_history', 'grade_grades_history', 'scale_history');
1402 foreach ($tables as $table) {
1403 if ($DB->delete_records_select($table, "timemodified < ?", array($histlifetime))) {
1404 mtrace(" Deleted old grade history records from '$table'");
1411 * Reset all course grades, refetch from the activities and recalculate
1413 * @param int $courseid The course to reset
1414 * @return bool success
1416 function grade_course_reset($courseid) {
1418 // no recalculations
1419 grade_force_full_regrading($courseid);
1421 $grade_items = grade_item::fetch_all(array('courseid'=>$courseid));
1422 foreach ($grade_items as $gid=>$grade_item) {
1423 $grade_item->delete_all_grades('reset');
1426 //refetch all grades
1427 grade_grab_course_grades($courseid);
1429 // recalculate all grades
1430 grade_regrade_final_grades($courseid);
1431 return true;
1435 * Convert a number to 5 decimal point float, an empty string or a null db compatible format
1436 * (we need this to decide if db value changed)
1438 * @param mixed $number The number to convert
1439 * @return mixed float or null
1441 function grade_floatval($number) {
1442 if (is_null($number) or $number === '') {
1443 return null;
1445 // we must round to 5 digits to get the same precision as in 10,5 db fields
1446 // note: db rounding for 10,5 is different from php round() function
1447 return round($number, 5);
1451 * Compare two float numbers safely. Uses 5 decimals php precision using {@link grade_floatval()}. Nulls accepted too.
1452 * Used for determining if a database update is required
1454 * @param float $f1 Float one to compare
1455 * @param float $f2 Float two to compare
1456 * @return bool True if the supplied values are different
1458 function grade_floats_different($f1, $f2) {
1459 // note: db rounding for 10,5 is different from php round() function
1460 return (grade_floatval($f1) !== grade_floatval($f2));
1464 * Compare two float numbers safely. Uses 5 decimals php precision using {@link grade_floatval()}
1466 * Do not use rounding for 10,5 at the database level as the results may be
1467 * different from php round() function.
1469 * @since Moodle 2.0
1470 * @param float $f1 Float one to compare
1471 * @param float $f2 Float two to compare
1472 * @return bool True if the values should be considered as the same grades
1474 function grade_floats_equal($f1, $f2) {
1475 return (grade_floatval($f1) === grade_floatval($f2));