MDL-45893 user_menu: revised based on action_menu
[moodle.git] / lib / gradelib.php
blob75a6f20df17e838e8cf478f27772c06b7bc749fb
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 one or more activities, 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 = null, $itemmodule = null, $iteminstance = null, $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 $params = array('courseid' => $courseid);
357 if (!empty($itemtype)) {
358 $params['itemtype'] = $itemtype;
360 if (!empty($itemmodule)) {
361 $params['itemmodule'] = $itemmodule;
363 if (!empty($iteminstance)) {
364 $params['iteminstance'] = $iteminstance;
366 if ($grade_items = grade_item::fetch_all($params)) {
367 foreach ($grade_items as $grade_item) {
368 $decimalpoints = null;
370 if (empty($grade_item->outcomeid)) {
371 // prepare information about grade item
372 $item = new stdClass();
373 $item->id = $grade_item->id;
374 $item->itemnumber = $grade_item->itemnumber;
375 $item->itemtype = $grade_item->itemtype;
376 $item->itemmodule = $grade_item->itemmodule;
377 $item->iteminstance = $grade_item->iteminstance;
378 $item->scaleid = $grade_item->scaleid;
379 $item->name = $grade_item->get_name();
380 $item->grademin = $grade_item->grademin;
381 $item->grademax = $grade_item->grademax;
382 $item->gradepass = $grade_item->gradepass;
383 $item->locked = $grade_item->is_locked();
384 $item->hidden = $grade_item->is_hidden();
385 $item->grades = array();
387 switch ($grade_item->gradetype) {
388 case GRADE_TYPE_NONE:
389 continue;
391 case GRADE_TYPE_VALUE:
392 $item->scaleid = 0;
393 break;
395 case GRADE_TYPE_TEXT:
396 $item->scaleid = 0;
397 $item->grademin = 0;
398 $item->grademax = 0;
399 $item->gradepass = 0;
400 break;
403 if (empty($userid_or_ids)) {
404 $userids = array();
406 } else if (is_array($userid_or_ids)) {
407 $userids = $userid_or_ids;
409 } else {
410 $userids = array($userid_or_ids);
413 if ($userids) {
414 $grade_grades = grade_grade::fetch_users_grades($grade_item, $userids, true);
415 foreach ($userids as $userid) {
416 $grade_grades[$userid]->grade_item =& $grade_item;
418 $grade = new stdClass();
419 $grade->grade = $grade_grades[$userid]->finalgrade;
420 $grade->locked = $grade_grades[$userid]->is_locked();
421 $grade->hidden = $grade_grades[$userid]->is_hidden();
422 $grade->overridden = $grade_grades[$userid]->overridden;
423 $grade->feedback = $grade_grades[$userid]->feedback;
424 $grade->feedbackformat = $grade_grades[$userid]->feedbackformat;
425 $grade->usermodified = $grade_grades[$userid]->usermodified;
426 $grade->datesubmitted = $grade_grades[$userid]->get_datesubmitted();
427 $grade->dategraded = $grade_grades[$userid]->get_dategraded();
429 // create text representation of grade
430 if ($grade_item->gradetype == GRADE_TYPE_TEXT or $grade_item->gradetype == GRADE_TYPE_NONE) {
431 $grade->grade = null;
432 $grade->str_grade = '-';
433 $grade->str_long_grade = $grade->str_grade;
435 } else if (in_array($grade_item->id, $needsupdate)) {
436 $grade->grade = false;
437 $grade->str_grade = get_string('error');
438 $grade->str_long_grade = $grade->str_grade;
440 } else if (is_null($grade->grade)) {
441 $grade->str_grade = '-';
442 $grade->str_long_grade = $grade->str_grade;
444 } else {
445 $grade->str_grade = grade_format_gradevalue($grade->grade, $grade_item);
446 if ($grade_item->gradetype == GRADE_TYPE_SCALE or $grade_item->get_displaytype() != GRADE_DISPLAY_TYPE_REAL) {
447 $grade->str_long_grade = $grade->str_grade;
448 } else {
449 $a = new stdClass();
450 $a->grade = $grade->str_grade;
451 $a->max = grade_format_gradevalue($grade_item->grademax, $grade_item);
452 $grade->str_long_grade = get_string('gradelong', 'grades', $a);
456 // create html representation of feedback
457 if (is_null($grade->feedback)) {
458 $grade->str_feedback = '';
459 } else {
460 $grade->str_feedback = format_text($grade->feedback, $grade->feedbackformat);
463 $item->grades[$userid] = $grade;
466 $return->items[$grade_item->itemnumber] = $item;
468 } else {
469 if (!$grade_outcome = grade_outcome::fetch(array('id'=>$grade_item->outcomeid))) {
470 debugging('Incorect outcomeid found');
471 continue;
474 // outcome info
475 $outcome = new stdClass();
476 $outcome->id = $grade_item->id;
477 $outcome->itemnumber = $grade_item->itemnumber;
478 $outcome->itemtype = $grade_item->itemtype;
479 $outcome->itemmodule = $grade_item->itemmodule;
480 $outcome->iteminstance = $grade_item->iteminstance;
481 $outcome->scaleid = $grade_outcome->scaleid;
482 $outcome->name = $grade_outcome->get_name();
483 $outcome->locked = $grade_item->is_locked();
484 $outcome->hidden = $grade_item->is_hidden();
486 if (empty($userid_or_ids)) {
487 $userids = array();
488 } else if (is_array($userid_or_ids)) {
489 $userids = $userid_or_ids;
490 } else {
491 $userids = array($userid_or_ids);
494 if ($userids) {
495 $grade_grades = grade_grade::fetch_users_grades($grade_item, $userids, true);
496 foreach ($userids as $userid) {
497 $grade_grades[$userid]->grade_item =& $grade_item;
499 $grade = new stdClass();
500 $grade->grade = $grade_grades[$userid]->finalgrade;
501 $grade->locked = $grade_grades[$userid]->is_locked();
502 $grade->hidden = $grade_grades[$userid]->is_hidden();
503 $grade->feedback = $grade_grades[$userid]->feedback;
504 $grade->feedbackformat = $grade_grades[$userid]->feedbackformat;
505 $grade->usermodified = $grade_grades[$userid]->usermodified;
507 // create text representation of grade
508 if (in_array($grade_item->id, $needsupdate)) {
509 $grade->grade = false;
510 $grade->str_grade = get_string('error');
512 } else if (is_null($grade->grade)) {
513 $grade->grade = 0;
514 $grade->str_grade = get_string('nooutcome', 'grades');
516 } else {
517 $grade->grade = (int)$grade->grade;
518 $scale = $grade_item->load_scale();
519 $grade->str_grade = format_string($scale->scale_items[(int)$grade->grade-1]);
522 // create html representation of feedback
523 if (is_null($grade->feedback)) {
524 $grade->str_feedback = '';
525 } else {
526 $grade->str_feedback = format_text($grade->feedback, $grade->feedbackformat);
529 $outcome->grades[$userid] = $grade;
533 if (isset($return->outcomes[$grade_item->itemnumber])) {
534 // itemnumber duplicates - lets fix them!
535 $newnumber = $grade_item->itemnumber + 1;
536 while(grade_item::fetch(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid, 'itemnumber'=>$newnumber))) {
537 $newnumber++;
539 $outcome->itemnumber = $newnumber;
540 $grade_item->itemnumber = $newnumber;
541 $grade_item->update('system');
544 $return->outcomes[$grade_item->itemnumber] = $outcome;
550 // sort results using itemnumbers
551 ksort($return->items, SORT_NUMERIC);
552 ksort($return->outcomes, SORT_NUMERIC);
554 return $return;
557 ///////////////////////////////////////////////////////////////////
558 ///// End of public API for communication with modules/blocks /////
559 ///////////////////////////////////////////////////////////////////
563 ///////////////////////////////////////////////////////////////////
564 ///// Internal API: used by gradebook plugins and Moodle core /////
565 ///////////////////////////////////////////////////////////////////
568 * Returns a course gradebook setting
570 * @param int $courseid
571 * @param string $name of setting, maybe null if reset only
572 * @param string $default value to return if setting is not found
573 * @param bool $resetcache force reset of internal static cache
574 * @return string value of the setting, $default if setting not found, NULL if supplied $name is null
576 function grade_get_setting($courseid, $name, $default=null, $resetcache=false) {
577 global $DB;
579 static $cache = array();
581 if ($resetcache or !array_key_exists($courseid, $cache)) {
582 $cache[$courseid] = array();
584 } else if (is_null($name)) {
585 return null;
587 } else if (array_key_exists($name, $cache[$courseid])) {
588 return $cache[$courseid][$name];
591 if (!$data = $DB->get_record('grade_settings', array('courseid'=>$courseid, 'name'=>$name))) {
592 $result = null;
593 } else {
594 $result = $data->value;
597 if (is_null($result)) {
598 $result = $default;
601 $cache[$courseid][$name] = $result;
602 return $result;
606 * Returns all course gradebook settings as object properties
608 * @param int $courseid
609 * @return object
611 function grade_get_settings($courseid) {
612 global $DB;
614 $settings = new stdClass();
615 $settings->id = $courseid;
617 if ($records = $DB->get_records('grade_settings', array('courseid'=>$courseid))) {
618 foreach ($records as $record) {
619 $settings->{$record->name} = $record->value;
623 return $settings;
627 * Add, update or delete a course gradebook setting
629 * @param int $courseid The course ID
630 * @param string $name Name of the setting
631 * @param string $value Value of the setting. NULL means delete the setting.
633 function grade_set_setting($courseid, $name, $value) {
634 global $DB;
636 if (is_null($value)) {
637 $DB->delete_records('grade_settings', array('courseid'=>$courseid, 'name'=>$name));
639 } else if (!$existing = $DB->get_record('grade_settings', array('courseid'=>$courseid, 'name'=>$name))) {
640 $data = new stdClass();
641 $data->courseid = $courseid;
642 $data->name = $name;
643 $data->value = $value;
644 $DB->insert_record('grade_settings', $data);
646 } else {
647 $data = new stdClass();
648 $data->id = $existing->id;
649 $data->value = $value;
650 $DB->update_record('grade_settings', $data);
653 grade_get_setting($courseid, null, null, true); // reset the cache
657 * Returns string representation of grade value
659 * @param float $value The grade value
660 * @param object $grade_item Grade item object passed by reference to prevent scale reloading
661 * @param bool $localized use localised decimal separator
662 * @param int $displaytype type of display. For example GRADE_DISPLAY_TYPE_REAL, GRADE_DISPLAY_TYPE_PERCENTAGE, GRADE_DISPLAY_TYPE_LETTER
663 * @param int $decimals The number of decimal places when displaying float values
664 * @return string
666 function grade_format_gradevalue($value, &$grade_item, $localized=true, $displaytype=null, $decimals=null) {
667 if ($grade_item->gradetype == GRADE_TYPE_NONE or $grade_item->gradetype == GRADE_TYPE_TEXT) {
668 return '';
671 // no grade yet?
672 if (is_null($value)) {
673 return '-';
676 if ($grade_item->gradetype != GRADE_TYPE_VALUE and $grade_item->gradetype != GRADE_TYPE_SCALE) {
677 //unknown type??
678 return '';
681 if (is_null($displaytype)) {
682 $displaytype = $grade_item->get_displaytype();
685 if (is_null($decimals)) {
686 $decimals = $grade_item->get_decimals();
689 switch ($displaytype) {
690 case GRADE_DISPLAY_TYPE_REAL:
691 return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized);
693 case GRADE_DISPLAY_TYPE_PERCENTAGE:
694 return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized);
696 case GRADE_DISPLAY_TYPE_LETTER:
697 return grade_format_gradevalue_letter($value, $grade_item);
699 case GRADE_DISPLAY_TYPE_REAL_PERCENTAGE:
700 return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ' (' .
701 grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ')';
703 case GRADE_DISPLAY_TYPE_REAL_LETTER:
704 return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ' (' .
705 grade_format_gradevalue_letter($value, $grade_item) . ')';
707 case GRADE_DISPLAY_TYPE_PERCENTAGE_REAL:
708 return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ' (' .
709 grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ')';
711 case GRADE_DISPLAY_TYPE_LETTER_REAL:
712 return grade_format_gradevalue_letter($value, $grade_item) . ' (' .
713 grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ')';
715 case GRADE_DISPLAY_TYPE_LETTER_PERCENTAGE:
716 return grade_format_gradevalue_letter($value, $grade_item) . ' (' .
717 grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ')';
719 case GRADE_DISPLAY_TYPE_PERCENTAGE_LETTER:
720 return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ' (' .
721 grade_format_gradevalue_letter($value, $grade_item) . ')';
722 default:
723 return '';
728 * Returns a float representation of a grade value
730 * @param float $value The grade value
731 * @param object $grade_item Grade item object
732 * @param int $decimals The number of decimal places
733 * @param bool $localized use localised decimal separator
734 * @return string
736 function grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) {
737 if ($grade_item->gradetype == GRADE_TYPE_SCALE) {
738 if (!$scale = $grade_item->load_scale()) {
739 return get_string('error');
742 $value = $grade_item->bounded_grade($value);
743 return format_string($scale->scale_items[$value-1]);
745 } else {
746 return format_float($value, $decimals, $localized);
751 * Returns a percentage representation of a grade value
753 * @param float $value The grade value
754 * @param object $grade_item Grade item object
755 * @param int $decimals The number of decimal places
756 * @param bool $localized use localised decimal separator
757 * @return string
759 function grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) {
760 $min = $grade_item->grademin;
761 $max = $grade_item->grademax;
762 if ($min == $max) {
763 return '';
765 $value = $grade_item->bounded_grade($value);
766 $percentage = (($value-$min)*100)/($max-$min);
767 return format_float($percentage, $decimals, $localized).' %';
771 * Returns a letter grade representation of a grade value
772 * The array of grade letters used is produced by {@link grade_get_letters()} using the course context
774 * @param float $value The grade value
775 * @param object $grade_item Grade item object
776 * @return string
778 function grade_format_gradevalue_letter($value, $grade_item) {
779 $context = context_course::instance($grade_item->courseid, IGNORE_MISSING);
780 if (!$letters = grade_get_letters($context)) {
781 return ''; // no letters??
784 if (is_null($value)) {
785 return '-';
788 $value = grade_grade::standardise_score($value, $grade_item->grademin, $grade_item->grademax, 0, 100);
789 $value = bounded_number(0, $value, 100); // just in case
790 foreach ($letters as $boundary => $letter) {
791 if ($value >= $boundary) {
792 return format_string($letter);
795 return '-'; // no match? maybe '' would be more correct
800 * Returns grade options for gradebook grade category menu
802 * @param int $courseid The course ID
803 * @param bool $includenew Include option for new category at array index -1
804 * @return array of grade categories in course
806 function grade_get_categories_menu($courseid, $includenew=false) {
807 $result = array();
808 if (!$categories = grade_category::fetch_all(array('courseid'=>$courseid))) {
809 //make sure course category exists
810 if (!grade_category::fetch_course_category($courseid)) {
811 debugging('Can not create course grade category!');
812 return $result;
814 $categories = grade_category::fetch_all(array('courseid'=>$courseid));
816 foreach ($categories as $key=>$category) {
817 if ($category->is_course_category()) {
818 $result[$category->id] = get_string('uncategorised', 'grades');
819 unset($categories[$key]);
822 if ($includenew) {
823 $result[-1] = get_string('newcategory', 'grades');
825 $cats = array();
826 foreach ($categories as $category) {
827 $cats[$category->id] = $category->get_name();
829 core_collator::asort($cats);
831 return ($result+$cats);
835 * Returns the array of grade letters to be used in the supplied context
837 * @param object $context Context object or null for defaults
838 * @return array of grade_boundary (minimum) => letter_string
840 function grade_get_letters($context=null) {
841 global $DB;
843 if (empty($context)) {
844 //default grading letters
845 return array('93'=>'A', '90'=>'A-', '87'=>'B+', '83'=>'B', '80'=>'B-', '77'=>'C+', '73'=>'C', '70'=>'C-', '67'=>'D+', '60'=>'D', '0'=>'F');
848 static $cache = array();
850 if (array_key_exists($context->id, $cache)) {
851 return $cache[$context->id];
854 if (count($cache) > 100) {
855 $cache = array(); // cache size limit
858 $letters = array();
860 $contexts = $context->get_parent_context_ids();
861 array_unshift($contexts, $context->id);
863 foreach ($contexts as $ctxid) {
864 if ($records = $DB->get_records('grade_letters', array('contextid'=>$ctxid), 'lowerboundary DESC')) {
865 foreach ($records as $record) {
866 $letters[$record->lowerboundary] = $record->letter;
870 if (!empty($letters)) {
871 $cache[$context->id] = $letters;
872 return $letters;
876 $letters = grade_get_letters(null);
877 $cache[$context->id] = $letters;
878 return $letters;
883 * Verify new value of grade item idnumber. Checks for uniqueness of new ID numbers. Old ID numbers are kept intact.
885 * @param string $idnumber string (with magic quotes)
886 * @param int $courseid ID numbers are course unique only
887 * @param grade_item $grade_item The grade item this idnumber is associated with
888 * @param stdClass $cm used for course module idnumbers and items attached to modules
889 * @return bool true means idnumber ok
891 function grade_verify_idnumber($idnumber, $courseid, $grade_item=null, $cm=null) {
892 global $DB;
894 if ($idnumber == '') {
895 //we allow empty idnumbers
896 return true;
899 // keep existing even when not unique
900 if ($cm and $cm->idnumber == $idnumber) {
901 if ($grade_item and $grade_item->itemnumber != 0) {
902 // grade item with itemnumber > 0 can't have the same idnumber as the main
903 // itemnumber 0 which is synced with course_modules
904 return false;
906 return true;
907 } else if ($grade_item and $grade_item->idnumber == $idnumber) {
908 return true;
911 if ($DB->record_exists('course_modules', array('course'=>$courseid, 'idnumber'=>$idnumber))) {
912 return false;
915 if ($DB->record_exists('grade_items', array('courseid'=>$courseid, 'idnumber'=>$idnumber))) {
916 return false;
919 return true;
923 * Force final grade recalculation in all course items
925 * @param int $courseid The course ID to recalculate
927 function grade_force_full_regrading($courseid) {
928 global $DB;
929 $DB->set_field('grade_items', 'needsupdate', 1, array('courseid'=>$courseid));
933 * Forces regrading of all site grades. Used when changing site setings
935 function grade_force_site_regrading() {
936 global $CFG, $DB;
937 $DB->set_field('grade_items', 'needsupdate', 1);
941 * Recover a user's grades from grade_grades_history
942 * @param int $userid the user ID whose grades we want to recover
943 * @param int $courseid the relevant course
944 * @return bool true if successful or false if there was an error or no grades could be recovered
946 function grade_recover_history_grades($userid, $courseid) {
947 global $CFG, $DB;
949 if ($CFG->disablegradehistory) {
950 debugging('Attempting to recover grades when grade history is disabled.');
951 return false;
954 //Were grades recovered? Flag to return.
955 $recoveredgrades = false;
957 //Check the user is enrolled in this course
958 //Dont bother checking if they have a gradeable role. They may get one later so recover
959 //whatever grades they have now just in case.
960 $course_context = context_course::instance($courseid);
961 if (!is_enrolled($course_context, $userid)) {
962 debugging('Attempting to recover the grades of a user who is deleted or not enrolled. Skipping recover.');
963 return false;
966 //Check for existing grades for this user in this course
967 //Recovering grades when the user already has grades can lead to duplicate indexes and bad data
968 //In the future we could move the existing grades to the history table then recover the grades from before then
969 $sql = "SELECT gg.id
970 FROM {grade_grades} gg
971 JOIN {grade_items} gi ON gi.id = gg.itemid
972 WHERE gi.courseid = :courseid AND gg.userid = :userid";
973 $params = array('userid' => $userid, 'courseid' => $courseid);
974 if ($DB->record_exists_sql($sql, $params)) {
975 debugging('Attempting to recover the grades of a user who already has grades. Skipping recover.');
976 return false;
977 } else {
978 //Retrieve the user's old grades
979 //have history ID as first column to guarantee we a unique first column
980 $sql = "SELECT h.id, gi.itemtype, gi.itemmodule, gi.iteminstance as iteminstance, gi.itemnumber, h.source, h.itemid, h.userid, h.rawgrade, h.rawgrademax,
981 h.rawgrademin, h.rawscaleid, h.usermodified, h.finalgrade, h.hidden, h.locked, h.locktime, h.exported, h.overridden, h.excluded, h.feedback,
982 h.feedbackformat, h.information, h.informationformat, h.timemodified, itemcreated.tm AS timecreated
983 FROM {grade_grades_history} h
984 JOIN (SELECT itemid, MAX(id) AS id
985 FROM {grade_grades_history}
986 WHERE userid = :userid1
987 GROUP BY itemid) maxquery ON h.id = maxquery.id AND h.itemid = maxquery.itemid
988 JOIN {grade_items} gi ON gi.id = h.itemid
989 JOIN (SELECT itemid, MAX(timemodified) AS tm
990 FROM {grade_grades_history}
991 WHERE userid = :userid2 AND action = :insertaction
992 GROUP BY itemid) itemcreated ON itemcreated.itemid = h.itemid
993 WHERE gi.courseid = :courseid";
994 $params = array('userid1' => $userid, 'userid2' => $userid , 'insertaction' => GRADE_HISTORY_INSERT, 'courseid' => $courseid);
995 $oldgrades = $DB->get_records_sql($sql, $params);
997 //now move the old grades to the grade_grades table
998 foreach ($oldgrades as $oldgrade) {
999 unset($oldgrade->id);
1001 $grade = new grade_grade($oldgrade, false);//2nd arg false as dont want to try and retrieve a record from the DB
1002 $grade->insert($oldgrade->source);
1004 //dont include default empty grades created when activities are created
1005 if (!is_null($oldgrade->finalgrade) || !is_null($oldgrade->feedback)) {
1006 $recoveredgrades = true;
1011 //Some activities require manual grade synching (moving grades from the activity into the gradebook)
1012 //If the student was deleted when synching was done they may have grades in the activity that haven't been moved across
1013 grade_grab_course_grades($courseid, null, $userid);
1015 return $recoveredgrades;
1019 * Updates all final grades in course.
1021 * @param int $courseid The course ID
1022 * @param int $userid If specified try to do a quick regrading of the grades of this user only
1023 * @param object $updated_item Optional grade item to be marked for regrading
1024 * @return bool true if ok, array of errors if problems found. Grade item id => error message
1026 function grade_regrade_final_grades($courseid, $userid=null, $updated_item=null) {
1028 $course_item = grade_item::fetch_course_item($courseid);
1030 if ($userid) {
1031 // one raw grade updated for one user
1032 if (empty($updated_item)) {
1033 print_error("cannotbenull", 'debug', '', "updated_item");
1035 if ($course_item->needsupdate) {
1036 $updated_item->force_regrading();
1037 return array($course_item->id =>'Can not do fast regrading after updating of raw grades');
1040 } else {
1041 if (!$course_item->needsupdate) {
1042 // nothing to do :-)
1043 return true;
1047 $grade_items = grade_item::fetch_all(array('courseid'=>$courseid));
1048 $depends_on = array();
1050 // first mark all category and calculated items as needing regrading
1051 // this is slower, but 100% accurate
1052 foreach ($grade_items as $gid=>$gitem) {
1053 if (!empty($updated_item) and $updated_item->id == $gid) {
1054 $grade_items[$gid]->needsupdate = 1;
1056 } else if ($gitem->is_course_item() or $gitem->is_category_item() or $gitem->is_calculated()) {
1057 $grade_items[$gid]->needsupdate = 1;
1060 // construct depends_on lookup array
1061 $depends_on[$gid] = $grade_items[$gid]->depends_on();
1064 $errors = array();
1065 $finalids = array();
1066 $gids = array_keys($grade_items);
1067 $failed = 0;
1069 while (count($finalids) < count($gids)) { // work until all grades are final or error found
1070 $count = 0;
1071 foreach ($gids as $gid) {
1072 if (in_array($gid, $finalids)) {
1073 continue; // already final
1076 if (!$grade_items[$gid]->needsupdate) {
1077 $finalids[] = $gid; // we can make it final - does not need update
1078 continue;
1081 $doupdate = true;
1082 foreach ($depends_on[$gid] as $did) {
1083 if (!in_array($did, $finalids)) {
1084 $doupdate = false;
1085 continue; // this item depends on something that is not yet in finals array
1089 //oki - let's update, calculate or aggregate :-)
1090 if ($doupdate) {
1091 $result = $grade_items[$gid]->regrade_final_grades($userid);
1093 if ($result === true) {
1094 $grade_items[$gid]->regrading_finished();
1095 $grade_items[$gid]->check_locktime(); // do the locktime item locking
1096 $count++;
1097 $finalids[] = $gid;
1099 } else {
1100 $grade_items[$gid]->force_regrading();
1101 $errors[$gid] = $result;
1106 if ($count == 0) {
1107 $failed++;
1108 } else {
1109 $failed = 0;
1112 if ($failed > 1) {
1113 foreach($gids as $gid) {
1114 if (in_array($gid, $finalids)) {
1115 continue; // this one is ok
1117 $grade_items[$gid]->force_regrading();
1118 $errors[$grade_items[$gid]->id] = get_string('errorcalculationbroken', 'grades');
1120 break; // Found error.
1124 if (count($errors) == 0) {
1125 if (empty($userid)) {
1126 // do the locktime locking of grades, but only when doing full regrading
1127 grade_grade::check_locktime_all($gids);
1129 return true;
1130 } else {
1131 return $errors;
1136 * Refetches grade data from course activities
1138 * @param int $courseid The course ID
1139 * @param string $modname Limit the grade fetch to a single module type. For example 'forum'
1140 * @param int $userid limit the grade fetch to a single user
1142 function grade_grab_course_grades($courseid, $modname=null, $userid=0) {
1143 global $CFG, $DB;
1145 if ($modname) {
1146 $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname
1147 FROM {".$modname."} a, {course_modules} cm, {modules} m
1148 WHERE m.name=:modname AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=:courseid";
1149 $params = array('modname'=>$modname, 'courseid'=>$courseid);
1151 if ($modinstances = $DB->get_records_sql($sql, $params)) {
1152 foreach ($modinstances as $modinstance) {
1153 grade_update_mod_grades($modinstance, $userid);
1156 return;
1159 if (!$mods = core_component::get_plugin_list('mod') ) {
1160 print_error('nomodules', 'debug');
1163 foreach ($mods as $mod => $fullmod) {
1164 if ($mod == 'NEWMODULE') { // Someone has unzipped the template, ignore it
1165 continue;
1168 // include the module lib once
1169 if (file_exists($fullmod.'/lib.php')) {
1170 // get all instance of the activity
1171 $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname
1172 FROM {".$mod."} a, {course_modules} cm, {modules} m
1173 WHERE m.name=:mod AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=:courseid";
1174 $params = array('mod'=>$mod, 'courseid'=>$courseid);
1176 if ($modinstances = $DB->get_records_sql($sql, $params)) {
1177 foreach ($modinstances as $modinstance) {
1178 grade_update_mod_grades($modinstance, $userid);
1186 * Force full update of module grades in central gradebook
1188 * @param object $modinstance Module object with extra cmidnumber and modname property
1189 * @param int $userid Optional user ID if limiting the update to a single user
1190 * @return bool True if success
1192 function grade_update_mod_grades($modinstance, $userid=0) {
1193 global $CFG, $DB;
1195 $fullmod = $CFG->dirroot.'/mod/'.$modinstance->modname;
1196 if (!file_exists($fullmod.'/lib.php')) {
1197 debugging('missing lib.php file in module ' . $modinstance->modname);
1198 return false;
1200 include_once($fullmod.'/lib.php');
1202 $updateitemfunc = $modinstance->modname.'_grade_item_update';
1203 $updategradesfunc = $modinstance->modname.'_update_grades';
1205 if (function_exists($updategradesfunc) and function_exists($updateitemfunc)) {
1206 //new grading supported, force updating of grades
1207 $updateitemfunc($modinstance);
1208 $updategradesfunc($modinstance, $userid);
1210 } else {
1211 // Module does not support grading?
1214 return true;
1218 * Remove grade letters for given context
1220 * @param context $context The context
1221 * @param bool $showfeedback If true a success notification will be displayed
1223 function remove_grade_letters($context, $showfeedback) {
1224 global $DB, $OUTPUT;
1226 $strdeleted = get_string('deleted');
1228 $DB->delete_records('grade_letters', array('contextid'=>$context->id));
1229 if ($showfeedback) {
1230 echo $OUTPUT->notification($strdeleted.' - '.get_string('letters', 'grades'), 'notifysuccess');
1235 * Remove all grade related course data
1236 * Grade history is kept
1238 * @param int $courseid The course ID
1239 * @param bool $showfeedback If true success notifications will be displayed
1241 function remove_course_grades($courseid, $showfeedback) {
1242 global $DB, $OUTPUT;
1244 $fs = get_file_storage();
1245 $strdeleted = get_string('deleted');
1247 $course_category = grade_category::fetch_course_category($courseid);
1248 $course_category->delete('coursedelete');
1249 $fs->delete_area_files(context_course::instance($courseid)->id, 'grade', 'feedback');
1250 if ($showfeedback) {
1251 echo $OUTPUT->notification($strdeleted.' - '.get_string('grades', 'grades').', '.get_string('items', 'grades').', '.get_string('categories', 'grades'), 'notifysuccess');
1254 if ($outcomes = grade_outcome::fetch_all(array('courseid'=>$courseid))) {
1255 foreach ($outcomes as $outcome) {
1256 $outcome->delete('coursedelete');
1259 $DB->delete_records('grade_outcomes_courses', array('courseid'=>$courseid));
1260 if ($showfeedback) {
1261 echo $OUTPUT->notification($strdeleted.' - '.get_string('outcomes', 'grades'), 'notifysuccess');
1264 if ($scales = grade_scale::fetch_all(array('courseid'=>$courseid))) {
1265 foreach ($scales as $scale) {
1266 $scale->delete('coursedelete');
1269 if ($showfeedback) {
1270 echo $OUTPUT->notification($strdeleted.' - '.get_string('scales'), 'notifysuccess');
1273 $DB->delete_records('grade_settings', array('courseid'=>$courseid));
1274 if ($showfeedback) {
1275 echo $OUTPUT->notification($strdeleted.' - '.get_string('settings', 'grades'), 'notifysuccess');
1280 * Called when course category is deleted
1281 * Cleans the gradebook of associated data
1283 * @param int $categoryid The course category id
1284 * @param int $newparentid If empty everything is deleted. Otherwise the ID of the category where content moved
1285 * @param bool $showfeedback print feedback
1287 function grade_course_category_delete($categoryid, $newparentid, $showfeedback) {
1288 global $DB;
1290 $context = context_coursecat::instance($categoryid);
1291 $DB->delete_records('grade_letters', array('contextid'=>$context->id));
1295 * Does gradebook cleanup when a module is uninstalled
1296 * Deletes all associated grade items
1298 * @param string $modname The grade item module name to remove. For example 'forum'
1300 function grade_uninstalled_module($modname) {
1301 global $CFG, $DB;
1303 $sql = "SELECT *
1304 FROM {grade_items}
1305 WHERE itemtype='mod' AND itemmodule=?";
1307 // go all items for this module and delete them including the grades
1308 $rs = $DB->get_recordset_sql($sql, array($modname));
1309 foreach ($rs as $item) {
1310 $grade_item = new grade_item($item, false);
1311 $grade_item->delete('moduninstall');
1313 $rs->close();
1317 * Deletes all of a user's grade data from gradebook
1319 * @param int $userid The user whose grade data should be deleted
1321 function grade_user_delete($userid) {
1322 if ($grades = grade_grade::fetch_all(array('userid'=>$userid))) {
1323 foreach ($grades as $grade) {
1324 $grade->delete('userdelete');
1330 * Purge course data when user unenrolls from a course
1332 * @param int $courseid The ID of the course the user has unenrolled from
1333 * @param int $userid The ID of the user unenrolling
1335 function grade_user_unenrol($courseid, $userid) {
1336 if ($items = grade_item::fetch_all(array('courseid'=>$courseid))) {
1337 foreach ($items as $item) {
1338 if ($grades = grade_grade::fetch_all(array('userid'=>$userid, 'itemid'=>$item->id))) {
1339 foreach ($grades as $grade) {
1340 $grade->delete('userdelete');
1348 * Grading cron job. Performs background clean up on the gradebook
1350 function grade_cron() {
1351 global $CFG, $DB;
1353 $now = time();
1355 $sql = "SELECT i.*
1356 FROM {grade_items} i
1357 WHERE i.locked = 0 AND i.locktime > 0 AND i.locktime < ? AND EXISTS (
1358 SELECT 'x' FROM {grade_items} c WHERE c.itemtype='course' AND c.needsupdate=0 AND c.courseid=i.courseid)";
1360 // go through all courses that have proper final grades and lock them if needed
1361 $rs = $DB->get_recordset_sql($sql, array($now));
1362 foreach ($rs as $item) {
1363 $grade_item = new grade_item($item, false);
1364 $grade_item->locked = $now;
1365 $grade_item->update('locktime');
1367 $rs->close();
1369 $grade_inst = new grade_grade();
1370 $fields = 'g.'.implode(',g.', $grade_inst->required_fields);
1372 $sql = "SELECT $fields
1373 FROM {grade_grades} g, {grade_items} i
1374 WHERE g.locked = 0 AND g.locktime > 0 AND g.locktime < ? AND g.itemid=i.id AND EXISTS (
1375 SELECT 'x' FROM {grade_items} c WHERE c.itemtype='course' AND c.needsupdate=0 AND c.courseid=i.courseid)";
1377 // go through all courses that have proper final grades and lock them if needed
1378 $rs = $DB->get_recordset_sql($sql, array($now));
1379 foreach ($rs as $grade) {
1380 $grade_grade = new grade_grade($grade, false);
1381 $grade_grade->locked = $now;
1382 $grade_grade->update('locktime');
1384 $rs->close();
1386 //TODO: do not run this cleanup every cron invocation
1387 // cleanup history tables
1388 if (!empty($CFG->gradehistorylifetime)) { // value in days
1389 $histlifetime = $now - ($CFG->gradehistorylifetime * 3600 * 24);
1390 $tables = array('grade_outcomes_history', 'grade_categories_history', 'grade_items_history', 'grade_grades_history', 'scale_history');
1391 foreach ($tables as $table) {
1392 if ($DB->delete_records_select($table, "timemodified < ?", array($histlifetime))) {
1393 mtrace(" Deleted old grade history records from '$table'");
1400 * Reset all course grades, refetch from the activities and recalculate
1402 * @param int $courseid The course to reset
1403 * @return bool success
1405 function grade_course_reset($courseid) {
1407 // no recalculations
1408 grade_force_full_regrading($courseid);
1410 $grade_items = grade_item::fetch_all(array('courseid'=>$courseid));
1411 foreach ($grade_items as $gid=>$grade_item) {
1412 $grade_item->delete_all_grades('reset');
1415 //refetch all grades
1416 grade_grab_course_grades($courseid);
1418 // recalculate all grades
1419 grade_regrade_final_grades($courseid);
1420 return true;
1424 * Convert a number to 5 decimal point float, an empty string or a null db compatible format
1425 * (we need this to decide if db value changed)
1427 * @param mixed $number The number to convert
1428 * @return mixed float or null
1430 function grade_floatval($number) {
1431 if (is_null($number) or $number === '') {
1432 return null;
1434 // we must round to 5 digits to get the same precision as in 10,5 db fields
1435 // note: db rounding for 10,5 is different from php round() function
1436 return round($number, 5);
1440 * Compare two float numbers safely. Uses 5 decimals php precision using {@link grade_floatval()}. Nulls accepted too.
1441 * Used for determining if a database update is required
1443 * @param float $f1 Float one to compare
1444 * @param float $f2 Float two to compare
1445 * @return bool True if the supplied values are different
1447 function grade_floats_different($f1, $f2) {
1448 // note: db rounding for 10,5 is different from php round() function
1449 return (grade_floatval($f1) !== grade_floatval($f2));
1453 * Compare two float numbers safely. Uses 5 decimals php precision using {@link grade_floatval()}
1455 * Do not use rounding for 10,5 at the database level as the results may be
1456 * different from php round() function.
1458 * @since Moodle 2.0
1459 * @param float $f1 Float one to compare
1460 * @param float $f2 Float two to compare
1461 * @return bool True if the values should be considered as the same grades
1463 function grade_floats_equal($f1, $f2) {
1464 return (grade_floatval($f1) === grade_floatval($f2));