8de67cf103d7bc2e5274325e8063c7843050c6a7
[moodle.git] / lib / gradelib.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Library of functions for gradebook - both public and internal
19  *
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
23  */
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.
44  *
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.
47  *
48  * Manual, course or category items can not be updated by this function.
49  *
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
60  */
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;
75     }
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;
88     }
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;
96             }
97         }
98         return GRADE_UPDATE_OK;
99     }
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']);
112                 }
113             }
115             foreach ($itemdetails as $k=>$v) {
116                 if (!in_array($k, $allowed)) {
117                     // ignore it
118                     continue;
119                 }
120                 if ($k == 'gradetype' and $v == GRADE_TYPE_NONE) {
121                     // no grade item needed!
122                     return GRADE_UPDATE_OK;
123                 }
124                 $params[$k] = $v;
125             }
126         }
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;
134         }
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;
143                 }
144                 if (in_array($k, $floats)) {
145                     if (grade_floats_different($grade_item->{$k}, $v)) {
146                         $grade_item->{$k} = $v;
147                         $update = true;
148                     }
150                 } else {
151                     if ($grade_item->{$k} != $v) {
152                         $grade_item->{$k} = $v;
153                         $update = true;
154                     }
155                 }
156             }
157             if ($update) {
158                 $grade_item->update();
159             }
160         }
161     }
163 /// reset grades if requested
164     if (!empty($itemdetails['reset'])) {
165         $grade_item->delete_all_grades('reset');
166         return GRADE_UPDATE_OK;
167     }
169 /// Some extra checks
170     // do we use grading?
171     if ($grade_item->gradetype == GRADE_TYPE_NONE) {
172         return GRADE_UPDATE_OK;
173     }
175     // no grade submitted
176     if (empty($grades)) {
177         return GRADE_UPDATE_OK;
178     }
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);
186         }
187     }
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;
194         }
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]);
199         }
200     }
202     if (empty($grades)) {
203         return GRADE_UPDATE_FAILED;
204     }
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);
215     }
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;
231             }
232             // existing grade requested
233             $grade       = $grades[$userid];
234             $grade_grade = new grade_grade($gd, false);
235             unset($grades[$userid]);
236             break;
237         }
239         if (is_null($grade_grade)) {
240             if (count($grades) == 0) {
241                 // no more grades to process
242                 break;
243             }
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]);
250         }
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'];
261         }
263         if (array_key_exists('feedback', $grade)) {
264             $feedback = $grade['feedback'];
265         }
267         if (array_key_exists('feedbackformat', $grade)) {
268             $feedbackformat = $grade['feedbackformat'];
269         }
271         if (array_key_exists('usermodified', $grade)) {
272             $usermodified = $grade['usermodified'];
273         }
275         if (array_key_exists('datesubmitted', $grade)) {
276             $datesubmitted = $grade['datesubmitted'];
277         }
279         if (array_key_exists('dategraded', $grade)) {
280             $dategraded = $grade['dategraded'];
281         }
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;
286         }
287     }
289     if ($rs) {
290         $rs->close();
291     }
293     if (!$failed) {
294         return GRADE_UPDATE_OK;
295     } else {
296         return GRADE_UPDATE_FAILED;
297     }
300 /**
301  * Updates a user's outcomes. Manual outcomes can not be updated.
302  *
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
312  */
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;
319             }
320             $grade = $data[$item->itemnumber] < 1 ? null : $data[$item->itemnumber];
321             $result = ($item->update_final_grade($userid, $grade, $source) && $result);
322         }
323         return $result;
324     }
325     return false; //grade items not found
328 /**
329  * Returns grading information for given activity, optionally with user grades
330  * Manual, course or category items can not be queried.
331  *
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
339  */
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);
353         }
354     }
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->itemnumber = $grade_item->itemnumber;
364                 $item->scaleid    = $grade_item->scaleid;
365                 $item->name       = $grade_item->get_name();
366                 $item->grademin   = $grade_item->grademin;
367                 $item->grademax   = $grade_item->grademax;
368                 $item->gradepass  = $grade_item->gradepass;
369                 $item->locked     = $grade_item->is_locked();
370                 $item->hidden     = $grade_item->is_hidden();
371                 $item->grades     = array();
373                 switch ($grade_item->gradetype) {
374                     case GRADE_TYPE_NONE:
375                         continue;
377                     case GRADE_TYPE_VALUE:
378                         $item->scaleid = 0;
379                         break;
381                     case GRADE_TYPE_TEXT:
382                         $item->scaleid   = 0;
383                         $item->grademin   = 0;
384                         $item->grademax   = 0;
385                         $item->gradepass  = 0;
386                         break;
387                 }
389                 if (empty($userid_or_ids)) {
390                     $userids = array();
392                 } else if (is_array($userid_or_ids)) {
393                     $userids = $userid_or_ids;
395                 } else {
396                     $userids = array($userid_or_ids);
397                 }
399                 if ($userids) {
400                     $grade_grades = grade_grade::fetch_users_grades($grade_item, $userids, true);
401                     foreach ($userids as $userid) {
402                         $grade_grades[$userid]->grade_item =& $grade_item;
404                         $grade = new stdClass();
405                         $grade->grade          = $grade_grades[$userid]->finalgrade;
406                         $grade->locked         = $grade_grades[$userid]->is_locked();
407                         $grade->hidden         = $grade_grades[$userid]->is_hidden();
408                         $grade->overridden     = $grade_grades[$userid]->overridden;
409                         $grade->feedback       = $grade_grades[$userid]->feedback;
410                         $grade->feedbackformat = $grade_grades[$userid]->feedbackformat;
411                         $grade->usermodified   = $grade_grades[$userid]->usermodified;
412                         $grade->datesubmitted  = $grade_grades[$userid]->get_datesubmitted();
413                         $grade->dategraded     = $grade_grades[$userid]->get_dategraded();
415                         // create text representation of grade
416                         if ($grade_item->gradetype == GRADE_TYPE_TEXT or $grade_item->gradetype == GRADE_TYPE_NONE) {
417                             $grade->grade          = null;
418                             $grade->str_grade      = '-';
419                             $grade->str_long_grade = $grade->str_grade;
421                         } else if (in_array($grade_item->id, $needsupdate)) {
422                             $grade->grade          = false;
423                             $grade->str_grade      = get_string('error');
424                             $grade->str_long_grade = $grade->str_grade;
426                         } else if (is_null($grade->grade)) {
427                             $grade->str_grade      = '-';
428                             $grade->str_long_grade = $grade->str_grade;
430                         } else {
431                             $grade->str_grade = grade_format_gradevalue($grade->grade, $grade_item);
432                             if ($grade_item->gradetype == GRADE_TYPE_SCALE or $grade_item->get_displaytype() != GRADE_DISPLAY_TYPE_REAL) {
433                                 $grade->str_long_grade = $grade->str_grade;
434                             } else {
435                                 $a = new stdClass();
436                                 $a->grade = $grade->str_grade;
437                                 $a->max   = grade_format_gradevalue($grade_item->grademax, $grade_item);
438                                 $grade->str_long_grade = get_string('gradelong', 'grades', $a);
439                             }
440                         }
442                         // create html representation of feedback
443                         if (is_null($grade->feedback)) {
444                             $grade->str_feedback = '';
445                         } else {
446                             $grade->str_feedback = format_text($grade->feedback, $grade->feedbackformat);
447                         }
449                         $item->grades[$userid] = $grade;
450                     }
451                 }
452                 $return->items[$grade_item->itemnumber] = $item;
454             } else {
455                 if (!$grade_outcome = grade_outcome::fetch(array('id'=>$grade_item->outcomeid))) {
456                     debugging('Incorect outcomeid found');
457                     continue;
458                 }
460                 // outcome info
461                 $outcome = new stdClass();
462                 $outcome->itemnumber = $grade_item->itemnumber;
463                 $outcome->scaleid    = $grade_outcome->scaleid;
464                 $outcome->name       = $grade_outcome->get_name();
465                 $outcome->locked     = $grade_item->is_locked();
466                 $outcome->hidden     = $grade_item->is_hidden();
468                 if (empty($userid_or_ids)) {
469                     $userids = array();
470                 } else if (is_array($userid_or_ids)) {
471                     $userids = $userid_or_ids;
472                 } else {
473                     $userids = array($userid_or_ids);
474                 }
476                 if ($userids) {
477                     $grade_grades = grade_grade::fetch_users_grades($grade_item, $userids, true);
478                     foreach ($userids as $userid) {
479                         $grade_grades[$userid]->grade_item =& $grade_item;
481                         $grade = new stdClass();
482                         $grade->grade          = $grade_grades[$userid]->finalgrade;
483                         $grade->locked         = $grade_grades[$userid]->is_locked();
484                         $grade->hidden         = $grade_grades[$userid]->is_hidden();
485                         $grade->feedback       = $grade_grades[$userid]->feedback;
486                         $grade->feedbackformat = $grade_grades[$userid]->feedbackformat;
487                         $grade->usermodified   = $grade_grades[$userid]->usermodified;
489                         // create text representation of grade
490                         if (in_array($grade_item->id, $needsupdate)) {
491                             $grade->grade     = false;
492                             $grade->str_grade = get_string('error');
494                         } else if (is_null($grade->grade)) {
495                             $grade->grade = 0;
496                             $grade->str_grade = get_string('nooutcome', 'grades');
498                         } else {
499                             $grade->grade = (int)$grade->grade;
500                             $scale = $grade_item->load_scale();
501                             $grade->str_grade = format_string($scale->scale_items[(int)$grade->grade-1]);
502                         }
504                         // create html representation of feedback
505                         if (is_null($grade->feedback)) {
506                             $grade->str_feedback = '';
507                         } else {
508                             $grade->str_feedback = format_text($grade->feedback, $grade->feedbackformat);
509                         }
511                         $outcome->grades[$userid] = $grade;
512                     }
513                 }
515                 if (isset($return->outcomes[$grade_item->itemnumber])) {
516                     // itemnumber duplicates - lets fix them!
517                     $newnumber = $grade_item->itemnumber + 1;
518                     while(grade_item::fetch(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid, 'itemnumber'=>$newnumber))) {
519                         $newnumber++;
520                     }
521                     $outcome->itemnumber    = $newnumber;
522                     $grade_item->itemnumber = $newnumber;
523                     $grade_item->update('system');
524                 }
526                 $return->outcomes[$grade_item->itemnumber] = $outcome;
528             }
529         }
530     }
532     // sort results using itemnumbers
533     ksort($return->items, SORT_NUMERIC);
534     ksort($return->outcomes, SORT_NUMERIC);
536     return $return;
539 ///////////////////////////////////////////////////////////////////
540 ///// End of public API for communication with modules/blocks /////
541 ///////////////////////////////////////////////////////////////////
545 ///////////////////////////////////////////////////////////////////
546 ///// Internal API: used by gradebook plugins and Moodle core /////
547 ///////////////////////////////////////////////////////////////////
549 /**
550  * Returns a  course gradebook setting
551  *
552  * @param int $courseid
553  * @param string $name of setting, maybe null if reset only
554  * @param string $default value to return if setting is not found
555  * @param bool $resetcache force reset of internal static cache
556  * @return string value of the setting, $default if setting not found, NULL if supplied $name is null
557  */
558 function grade_get_setting($courseid, $name, $default=null, $resetcache=false) {
559     global $DB;
561     static $cache = array();
563     if ($resetcache or !array_key_exists($courseid, $cache)) {
564         $cache[$courseid] = array();
566     } else if (is_null($name)) {
567         return null;
569     } else if (array_key_exists($name, $cache[$courseid])) {
570         return $cache[$courseid][$name];
571     }
573     if (!$data = $DB->get_record('grade_settings', array('courseid'=>$courseid, 'name'=>$name))) {
574         $result = null;
575     } else {
576         $result = $data->value;
577     }
579     if (is_null($result)) {
580         $result = $default;
581     }
583     $cache[$courseid][$name] = $result;
584     return $result;
587 /**
588  * Returns all course gradebook settings as object properties
589  *
590  * @param int $courseid
591  * @return object
592  */
593 function grade_get_settings($courseid) {
594     global $DB;
596      $settings = new stdClass();
597      $settings->id = $courseid;
599     if ($records = $DB->get_records('grade_settings', array('courseid'=>$courseid))) {
600         foreach ($records as $record) {
601             $settings->{$record->name} = $record->value;
602         }
603     }
605     return $settings;
608 /**
609  * Add, update or delete a course gradebook setting
610  *
611  * @param int $courseid The course ID
612  * @param string $name Name of the setting
613  * @param string $value Value of the setting. NULL means delete the setting.
614  */
615 function grade_set_setting($courseid, $name, $value) {
616     global $DB;
618     if (is_null($value)) {
619         $DB->delete_records('grade_settings', array('courseid'=>$courseid, 'name'=>$name));
621     } else if (!$existing = $DB->get_record('grade_settings', array('courseid'=>$courseid, 'name'=>$name))) {
622         $data = new stdClass();
623         $data->courseid = $courseid;
624         $data->name     = $name;
625         $data->value    = $value;
626         $DB->insert_record('grade_settings', $data);
628     } else {
629         $data = new stdClass();
630         $data->id       = $existing->id;
631         $data->value    = $value;
632         $DB->update_record('grade_settings', $data);
633     }
635     grade_get_setting($courseid, null, null, true); // reset the cache
638 /**
639  * Returns string representation of grade value
640  *
641  * @param float $value The grade value
642  * @param object $grade_item Grade item object passed by reference to prevent scale reloading
643  * @param bool $localized use localised decimal separator
644  * @param int $displaytype type of display. For example GRADE_DISPLAY_TYPE_REAL, GRADE_DISPLAY_TYPE_PERCENTAGE, GRADE_DISPLAY_TYPE_LETTER
645  * @param int $decimals The number of decimal places when displaying float values
646  * @return string
647  */
648 function grade_format_gradevalue($value, &$grade_item, $localized=true, $displaytype=null, $decimals=null) {
649     if ($grade_item->gradetype == GRADE_TYPE_NONE or $grade_item->gradetype == GRADE_TYPE_TEXT) {
650         return '';
651     }
653     // no grade yet?
654     if (is_null($value)) {
655         return '-';
656     }
658     if ($grade_item->gradetype != GRADE_TYPE_VALUE and $grade_item->gradetype != GRADE_TYPE_SCALE) {
659         //unknown type??
660         return '';
661     }
663     if (is_null($displaytype)) {
664         $displaytype = $grade_item->get_displaytype();
665     }
667     if (is_null($decimals)) {
668         $decimals = $grade_item->get_decimals();
669     }
671     switch ($displaytype) {
672         case GRADE_DISPLAY_TYPE_REAL:
673             return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized);
675         case GRADE_DISPLAY_TYPE_PERCENTAGE:
676             return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized);
678         case GRADE_DISPLAY_TYPE_LETTER:
679             return grade_format_gradevalue_letter($value, $grade_item);
681         case GRADE_DISPLAY_TYPE_REAL_PERCENTAGE:
682             return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ' (' .
683                     grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ')';
685         case GRADE_DISPLAY_TYPE_REAL_LETTER:
686             return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ' (' .
687                     grade_format_gradevalue_letter($value, $grade_item) . ')';
689         case GRADE_DISPLAY_TYPE_PERCENTAGE_REAL:
690             return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ' (' .
691                     grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ')';
693         case GRADE_DISPLAY_TYPE_LETTER_REAL:
694             return grade_format_gradevalue_letter($value, $grade_item) . ' (' .
695                     grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ')';
697         case GRADE_DISPLAY_TYPE_LETTER_PERCENTAGE:
698             return grade_format_gradevalue_letter($value, $grade_item) . ' (' .
699                     grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ')';
701         case GRADE_DISPLAY_TYPE_PERCENTAGE_LETTER:
702             return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ' (' .
703                     grade_format_gradevalue_letter($value, $grade_item) . ')';
704         default:
705             return '';
706     }
709 /**
710  * Returns a float representation of a grade value
711  *
712  * @param float $value The grade value
713  * @param object $grade_item Grade item object
714  * @param int $decimals The number of decimal places
715  * @param bool $localized use localised decimal separator
716  * @return string
717  */
718 function grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) {
719     if ($grade_item->gradetype == GRADE_TYPE_SCALE) {
720         if (!$scale = $grade_item->load_scale()) {
721             return get_string('error');
722         }
724         $value = $grade_item->bounded_grade($value);
725         return format_string($scale->scale_items[$value-1]);
727     } else {
728         return format_float($value, $decimals, $localized);
729     }
732 /**
733  * Returns a percentage representation of a grade value
734  *
735  * @param float $value The grade value
736  * @param object $grade_item Grade item object
737  * @param int $decimals The number of decimal places
738  * @param bool $localized use localised decimal separator
739  * @return string
740  */
741 function grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) {
742     $min = $grade_item->grademin;
743     $max = $grade_item->grademax;
744     if ($min == $max) {
745         return '';
746     }
747     $value = $grade_item->bounded_grade($value);
748     $percentage = (($value-$min)*100)/($max-$min);
749     return format_float($percentage, $decimals, $localized).' %';
752 /**
753  * Returns a letter grade representation of a grade value
754  * The array of grade letters used is produced by {@link grade_get_letters()} using the course context
755  *
756  * @param float $value The grade value
757  * @param object $grade_item Grade item object
758  * @return string
759  */
760 function grade_format_gradevalue_letter($value, $grade_item) {
761     $context = get_context_instance(CONTEXT_COURSE, $grade_item->courseid);
762     if (!$letters = grade_get_letters($context)) {
763         return ''; // no letters??
764     }
766     if (is_null($value)) {
767         return '-';
768     }
770     $value = grade_grade::standardise_score($value, $grade_item->grademin, $grade_item->grademax, 0, 100);
771     $value = bounded_number(0, $value, 100); // just in case
772     foreach ($letters as $boundary => $letter) {
773         if ($value >= $boundary) {
774             return format_string($letter);
775         }
776     }
777     return '-'; // no match? maybe '' would be more correct
781 /**
782  * Returns grade options for gradebook grade category menu
783  *
784  * @param int $courseid The course ID
785  * @param bool $includenew Include option for new category at array index -1
786  * @return array of grade categories in course
787  */
788 function grade_get_categories_menu($courseid, $includenew=false) {
789     $result = array();
790     if (!$categories = grade_category::fetch_all(array('courseid'=>$courseid))) {
791         //make sure course category exists
792         if (!grade_category::fetch_course_category($courseid)) {
793             debugging('Can not create course grade category!');
794             return $result;
795         }
796         $categories = grade_category::fetch_all(array('courseid'=>$courseid));
797     }
798     foreach ($categories as $key=>$category) {
799         if ($category->is_course_category()) {
800             $result[$category->id] = get_string('uncategorised', 'grades');
801             unset($categories[$key]);
802         }
803     }
804     if ($includenew) {
805         $result[-1] = get_string('newcategory', 'grades');
806     }
807     $cats = array();
808     foreach ($categories as $category) {
809         $cats[$category->id] = $category->get_name();
810     }
811     collatorlib::asort($cats);
813     return ($result+$cats);
816 /**
817  * Returns the array of grade letters to be used in the supplied context
818  *
819  * @param object $context Context object or null for defaults
820  * @return array of grade_boundary (minimum) => letter_string
821  */
822 function grade_get_letters($context=null) {
823     global $DB;
825     if (empty($context)) {
826         //default grading letters
827         return array('93'=>'A', '90'=>'A-', '87'=>'B+', '83'=>'B', '80'=>'B-', '77'=>'C+', '73'=>'C', '70'=>'C-', '67'=>'D+', '60'=>'D', '0'=>'F');
828     }
830     static $cache = array();
832     if (array_key_exists($context->id, $cache)) {
833         return $cache[$context->id];
834     }
836     if (count($cache) > 100) {
837         $cache = array(); // cache size limit
838     }
840     $letters = array();
842     $contexts = get_parent_contexts($context);
843     array_unshift($contexts, $context->id);
845     foreach ($contexts as $ctxid) {
846         if ($records = $DB->get_records('grade_letters', array('contextid'=>$ctxid), 'lowerboundary DESC')) {
847             foreach ($records as $record) {
848                 $letters[$record->lowerboundary] = $record->letter;
849             }
850         }
852         if (!empty($letters)) {
853             $cache[$context->id] = $letters;
854             return $letters;
855         }
856     }
858     $letters = grade_get_letters(null);
859     $cache[$context->id] = $letters;
860     return $letters;
864 /**
865  * Verify new value of grade item idnumber. Checks for uniqueness of new ID numbers. Old ID numbers are kept intact.
866  *
867  * @param string $idnumber string (with magic quotes)
868  * @param int $courseid ID numbers are course unique only
869  * @param grade_item $grade_item The grade item this idnumber is associated with
870  * @param stdClass $cm used for course module idnumbers and items attached to modules
871  * @return bool true means idnumber ok
872  */
873 function grade_verify_idnumber($idnumber, $courseid, $grade_item=null, $cm=null) {
874     global $DB;
876     if ($idnumber == '') {
877         //we allow empty idnumbers
878         return true;
879     }
881     // keep existing even when not unique
882     if ($cm and $cm->idnumber == $idnumber) {
883         if ($grade_item and $grade_item->itemnumber != 0) {
884             // grade item with itemnumber > 0 can't have the same idnumber as the main
885             // itemnumber 0 which is synced with course_modules
886             return false;
887         }
888         return true;
889     } else if ($grade_item and $grade_item->idnumber == $idnumber) {
890         return true;
891     }
893     if ($DB->record_exists('course_modules', array('course'=>$courseid, 'idnumber'=>$idnumber))) {
894         return false;
895     }
897     if ($DB->record_exists('grade_items', array('courseid'=>$courseid, 'idnumber'=>$idnumber))) {
898         return false;
899     }
901     return true;
904 /**
905  * Force final grade recalculation in all course items
906  *
907  * @param int $courseid The course ID to recalculate
908  */
909 function grade_force_full_regrading($courseid) {
910     global $DB;
911     $DB->set_field('grade_items', 'needsupdate', 1, array('courseid'=>$courseid));
914 /**
915  * Forces regrading of all site grades. Used when changing site setings
916  */
917 function grade_force_site_regrading() {
918     global $CFG, $DB;
919     $DB->set_field('grade_items', 'needsupdate', 1);
922 /**
923  * Recover a user's grades from grade_grades_history
924  * @param int $userid the user ID whose grades we want to recover
925  * @param int $courseid the relevant course
926  * @return bool true if successful or false if there was an error or no grades could be recovered
927  */
928 function grade_recover_history_grades($userid, $courseid) {
929     global $CFG, $DB;
931     if ($CFG->disablegradehistory) {
932         debugging('Attempting to recover grades when grade history is disabled.');
933         return false;
934     }
936     //Were grades recovered? Flag to return.
937     $recoveredgrades = false;
939     //Check the user is enrolled in this course
940     //Dont bother checking if they have a gradeable role. They may get one later so recover
941     //whatever grades they have now just in case.
942     $course_context = get_context_instance(CONTEXT_COURSE, $courseid);
943     if (!is_enrolled($course_context, $userid)) {
944         debugging('Attempting to recover the grades of a user who is deleted or not enrolled. Skipping recover.');
945         return false;
946     }
948     //Check for existing grades for this user in this course
949     //Recovering grades when the user already has grades can lead to duplicate indexes and bad data
950     //In the future we could move the existing grades to the history table then recover the grades from before then
951     $sql = "SELECT gg.id
952               FROM {grade_grades} gg
953               JOIN {grade_items} gi ON gi.id = gg.itemid
954              WHERE gi.courseid = :courseid AND gg.userid = :userid";
955     $params = array('userid' => $userid, 'courseid' => $courseid);
956     if ($DB->record_exists_sql($sql, $params)) {
957         debugging('Attempting to recover the grades of a user who already has grades. Skipping recover.');
958         return false;
959     } else {
960         //Retrieve the user's old grades
961         //have history ID as first column to guarantee we a unique first column
962         $sql = "SELECT h.id, gi.itemtype, gi.itemmodule, gi.iteminstance as iteminstance, gi.itemnumber, h.source, h.itemid, h.userid, h.rawgrade, h.rawgrademax,
963                        h.rawgrademin, h.rawscaleid, h.usermodified, h.finalgrade, h.hidden, h.locked, h.locktime, h.exported, h.overridden, h.excluded, h.feedback,
964                        h.feedbackformat, h.information, h.informationformat, h.timemodified, itemcreated.tm AS timecreated
965                   FROM {grade_grades_history} h
966                   JOIN (SELECT itemid, MAX(id) AS id
967                           FROM {grade_grades_history}
968                          WHERE userid = :userid1
969                       GROUP BY itemid) maxquery ON h.id = maxquery.id AND h.itemid = maxquery.itemid
970                   JOIN {grade_items} gi ON gi.id = h.itemid
971                   JOIN (SELECT itemid, MAX(timemodified) AS tm
972                           FROM {grade_grades_history}
973                          WHERE userid = :userid2 AND action = :insertaction
974                       GROUP BY itemid) itemcreated ON itemcreated.itemid = h.itemid
975                  WHERE gi.courseid = :courseid";
976         $params = array('userid1' => $userid, 'userid2' => $userid , 'insertaction' => GRADE_HISTORY_INSERT, 'courseid' => $courseid);
977         $oldgrades = $DB->get_records_sql($sql, $params);
979         //now move the old grades to the grade_grades table
980         foreach ($oldgrades as $oldgrade) {
981             unset($oldgrade->id);
983             $grade = new grade_grade($oldgrade, false);//2nd arg false as dont want to try and retrieve a record from the DB
984             $grade->insert($oldgrade->source);
986             //dont include default empty grades created when activities are created
987             if (!is_null($oldgrade->finalgrade) || !is_null($oldgrade->feedback)) {
988                 $recoveredgrades = true;
989             }
990         }
991     }
993     //Some activities require manual grade synching (moving grades from the activity into the gradebook)
994     //If the student was deleted when synching was done they may have grades in the activity that haven't been moved across
995     grade_grab_course_grades($courseid, null, $userid);
997     return $recoveredgrades;
1000 /**
1001  * Updates all final grades in course.
1002  *
1003  * @param int $courseid The course ID
1004  * @param int $userid If specified try to do a quick regrading of the grades of this user only
1005  * @param object $updated_item Optional grade item to be marked for regrading
1006  * @return bool true if ok, array of errors if problems found. Grade item id => error message
1007  */
1008 function grade_regrade_final_grades($courseid, $userid=null, $updated_item=null) {
1010     $course_item = grade_item::fetch_course_item($courseid);
1012     if ($userid) {
1013         // one raw grade updated for one user
1014         if (empty($updated_item)) {
1015             print_error("cannotbenull", 'debug', '', "updated_item");
1016         }
1017         if ($course_item->needsupdate) {
1018             $updated_item->force_regrading();
1019             return array($course_item->id =>'Can not do fast regrading after updating of raw grades');
1020         }
1022     } else {
1023         if (!$course_item->needsupdate) {
1024             // nothing to do :-)
1025             return true;
1026         }
1027     }
1029     $grade_items = grade_item::fetch_all(array('courseid'=>$courseid));
1030     $depends_on = array();
1032     // first mark all category and calculated items as needing regrading
1033     // this is slower, but 100% accurate
1034     foreach ($grade_items as $gid=>$gitem) {
1035         if (!empty($updated_item) and $updated_item->id == $gid) {
1036             $grade_items[$gid]->needsupdate = 1;
1038         } else if ($gitem->is_course_item() or $gitem->is_category_item() or $gitem->is_calculated()) {
1039             $grade_items[$gid]->needsupdate = 1;
1040         }
1042         // construct depends_on lookup array
1043         $depends_on[$gid] = $grade_items[$gid]->depends_on();
1044     }
1046     $errors = array();
1047     $finalids = array();
1048     $gids     = array_keys($grade_items);
1049     $failed = 0;
1051     while (count($finalids) < count($gids)) { // work until all grades are final or error found
1052         $count = 0;
1053         foreach ($gids as $gid) {
1054             if (in_array($gid, $finalids)) {
1055                 continue; // already final
1056             }
1058             if (!$grade_items[$gid]->needsupdate) {
1059                 $finalids[] = $gid; // we can make it final - does not need update
1060                 continue;
1061             }
1063             $doupdate = true;
1064             foreach ($depends_on[$gid] as $did) {
1065                 if (!in_array($did, $finalids)) {
1066                     $doupdate = false;
1067                     continue; // this item depends on something that is not yet in finals array
1068                 }
1069             }
1071             //oki - let's update, calculate or aggregate :-)
1072             if ($doupdate) {
1073                 $result = $grade_items[$gid]->regrade_final_grades($userid);
1075                 if ($result === true) {
1076                     $grade_items[$gid]->regrading_finished();
1077                     $grade_items[$gid]->check_locktime(); // do the locktime item locking
1078                     $count++;
1079                     $finalids[] = $gid;
1081                 } else {
1082                     $grade_items[$gid]->force_regrading();
1083                     $errors[$gid] = $result;
1084                 }
1085             }
1086         }
1088         if ($count == 0) {
1089             $failed++;
1090         } else {
1091             $failed = 0;
1092         }
1094         if ($failed > 1) {
1095             foreach($gids as $gid) {
1096                 if (in_array($gid, $finalids)) {
1097                     continue; // this one is ok
1098                 }
1099                 $grade_items[$gid]->force_regrading();
1100                 $errors[$grade_items[$gid]->id] = 'Probably circular reference or broken calculation formula'; // TODO: localize
1101             }
1102             break; // oki, found error
1103         }
1104     }
1106     if (count($errors) == 0) {
1107         if (empty($userid)) {
1108             // do the locktime locking of grades, but only when doing full regrading
1109             grade_grade::check_locktime_all($gids);
1110         }
1111         return true;
1112     } else {
1113         return $errors;
1114     }
1117 /**
1118  * Refetches grade data from course activities
1119  *
1120  * @param int $courseid The course ID
1121  * @param string $modname Limit the grade fetch to a single module type. For example 'forum'
1122  * @param int $userid limit the grade fetch to a single user
1123  */
1124 function grade_grab_course_grades($courseid, $modname=null, $userid=0) {
1125     global $CFG, $DB;
1127     if ($modname) {
1128         $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname
1129                   FROM {".$modname."} a, {course_modules} cm, {modules} m
1130                  WHERE m.name=:modname AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=:courseid";
1131         $params = array('modname'=>$modname, 'courseid'=>$courseid);
1133         if ($modinstances = $DB->get_records_sql($sql, $params)) {
1134             foreach ($modinstances as $modinstance) {
1135                 grade_update_mod_grades($modinstance, $userid);
1136             }
1137         }
1138         return;
1139     }
1141     if (!$mods = get_plugin_list('mod') ) {
1142         print_error('nomodules', 'debug');
1143     }
1145     foreach ($mods as $mod => $fullmod) {
1146         if ($mod == 'NEWMODULE') {   // Someone has unzipped the template, ignore it
1147             continue;
1148         }
1150         // include the module lib once
1151         if (file_exists($fullmod.'/lib.php')) {
1152             // get all instance of the activity
1153             $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname
1154                       FROM {".$mod."} a, {course_modules} cm, {modules} m
1155                      WHERE m.name=:mod AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=:courseid";
1156             $params = array('mod'=>$mod, 'courseid'=>$courseid);
1158             if ($modinstances = $DB->get_records_sql($sql, $params)) {
1159                 foreach ($modinstances as $modinstance) {
1160                     grade_update_mod_grades($modinstance, $userid);
1161                 }
1162             }
1163         }
1164     }
1167 /**
1168  * Force full update of module grades in central gradebook
1169  *
1170  * @param object $modinstance Module object with extra cmidnumber and modname property
1171  * @param int $userid Optional user ID if limiting the update to a single user
1172  * @return bool True if success
1173  */
1174 function grade_update_mod_grades($modinstance, $userid=0) {
1175     global $CFG, $DB;
1177     $fullmod = $CFG->dirroot.'/mod/'.$modinstance->modname;
1178     if (!file_exists($fullmod.'/lib.php')) {
1179         debugging('missing lib.php file in module ' . $modinstance->modname);
1180         return false;
1181     }
1182     include_once($fullmod.'/lib.php');
1184     $updateitemfunc   = $modinstance->modname.'_grade_item_update';
1185     $updategradesfunc = $modinstance->modname.'_update_grades';
1187     if (function_exists($updategradesfunc) and function_exists($updateitemfunc)) {
1188         //new grading supported, force updating of grades
1189         $updateitemfunc($modinstance);
1190         $updategradesfunc($modinstance, $userid);
1192     } else {
1193         // mudule does not support grading??
1194     }
1196     return true;
1199 /**
1200  * Remove grade letters for given context
1201  *
1202  * @param context $context The context
1203  * @param bool $showfeedback If true a success notification will be displayed
1204  */
1205 function remove_grade_letters($context, $showfeedback) {
1206     global $DB, $OUTPUT;
1208     $strdeleted = get_string('deleted');
1210     $DB->delete_records('grade_letters', array('contextid'=>$context->id));
1211     if ($showfeedback) {
1212         echo $OUTPUT->notification($strdeleted.' - '.get_string('letters', 'grades'), 'notifysuccess');
1213     }
1216 /**
1217  * Remove all grade related course data
1218  * Grade history is kept
1219  *
1220  * @param int $courseid The course ID
1221  * @param bool $showfeedback If true success notifications will be displayed
1222  */
1223 function remove_course_grades($courseid, $showfeedback) {
1224     global $DB, $OUTPUT;
1226     $fs = get_file_storage();
1227     $strdeleted = get_string('deleted');
1229     $course_category = grade_category::fetch_course_category($courseid);
1230     $course_category->delete('coursedelete');
1231     $fs->delete_area_files(get_context_instance(CONTEXT_COURSE, $courseid)->id, 'grade', 'feedback');
1232     if ($showfeedback) {
1233         echo $OUTPUT->notification($strdeleted.' - '.get_string('grades', 'grades').', '.get_string('items', 'grades').', '.get_string('categories', 'grades'), 'notifysuccess');
1234     }
1236     if ($outcomes = grade_outcome::fetch_all(array('courseid'=>$courseid))) {
1237         foreach ($outcomes as $outcome) {
1238             $outcome->delete('coursedelete');
1239         }
1240     }
1241     $DB->delete_records('grade_outcomes_courses', array('courseid'=>$courseid));
1242     if ($showfeedback) {
1243         echo $OUTPUT->notification($strdeleted.' - '.get_string('outcomes', 'grades'), 'notifysuccess');
1244     }
1246     if ($scales = grade_scale::fetch_all(array('courseid'=>$courseid))) {
1247         foreach ($scales as $scale) {
1248             $scale->delete('coursedelete');
1249         }
1250     }
1251     if ($showfeedback) {
1252         echo $OUTPUT->notification($strdeleted.' - '.get_string('scales'), 'notifysuccess');
1253     }
1255     $DB->delete_records('grade_settings', array('courseid'=>$courseid));
1256     if ($showfeedback) {
1257         echo $OUTPUT->notification($strdeleted.' - '.get_string('settings', 'grades'), 'notifysuccess');
1258     }
1261 /**
1262  * Called when course category is deleted
1263  * Cleans the gradebook of associated data
1264  *
1265  * @param int $categoryid The course category id
1266  * @param int $newparentid If empty everything is deleted. Otherwise the ID of the category where content moved
1267  * @param bool $showfeedback print feedback
1268  */
1269 function grade_course_category_delete($categoryid, $newparentid, $showfeedback) {
1270     global $DB;
1272     $context = get_context_instance(CONTEXT_COURSECAT, $categoryid);
1273     $DB->delete_records('grade_letters', array('contextid'=>$context->id));
1276 /**
1277  * Does gradebook cleanup when a module is uninstalled
1278  * Deletes all associated grade items
1279  *
1280  * @param string $modname The grade item module name to remove. For example 'forum'
1281  */
1282 function grade_uninstalled_module($modname) {
1283     global $CFG, $DB;
1285     $sql = "SELECT *
1286               FROM {grade_items}
1287              WHERE itemtype='mod' AND itemmodule=?";
1289     // go all items for this module and delete them including the grades
1290     $rs = $DB->get_recordset_sql($sql, array($modname));
1291     foreach ($rs as $item) {
1292         $grade_item = new grade_item($item, false);
1293         $grade_item->delete('moduninstall');
1294     }
1295     $rs->close();
1298 /**
1299  * Deletes all of a user's grade data from gradebook
1300  *
1301  * @param int $userid The user whose grade data should be deleted
1302  */
1303 function grade_user_delete($userid) {
1304     if ($grades = grade_grade::fetch_all(array('userid'=>$userid))) {
1305         foreach ($grades as $grade) {
1306             $grade->delete('userdelete');
1307         }
1308     }
1311 /**
1312  * Purge course data when user unenrolls from a course
1313  *
1314  * @param int $courseid The ID of the course the user has unenrolled from
1315  * @param int $userid The ID of the user unenrolling
1316  */
1317 function grade_user_unenrol($courseid, $userid) {
1318     if ($items = grade_item::fetch_all(array('courseid'=>$courseid))) {
1319         foreach ($items as $item) {
1320             if ($grades = grade_grade::fetch_all(array('userid'=>$userid, 'itemid'=>$item->id))) {
1321                 foreach ($grades as $grade) {
1322                     $grade->delete('userdelete');
1323                 }
1324             }
1325         }
1326     }
1329 /**
1330  * Grading cron job. Performs background clean up on the gradebook
1331  */
1332 function grade_cron() {
1333     global $CFG, $DB;
1335     $now = time();
1337     $sql = "SELECT i.*
1338               FROM {grade_items} i
1339              WHERE i.locked = 0 AND i.locktime > 0 AND i.locktime < ? AND EXISTS (
1340                 SELECT 'x' FROM {grade_items} c WHERE c.itemtype='course' AND c.needsupdate=0 AND c.courseid=i.courseid)";
1342     // go through all courses that have proper final grades and lock them if needed
1343     $rs = $DB->get_recordset_sql($sql, array($now));
1344     foreach ($rs as $item) {
1345         $grade_item = new grade_item($item, false);
1346         $grade_item->locked = $now;
1347         $grade_item->update('locktime');
1348     }
1349     $rs->close();
1351     $grade_inst = new grade_grade();
1352     $fields = 'g.'.implode(',g.', $grade_inst->required_fields);
1354     $sql = "SELECT $fields
1355               FROM {grade_grades} g, {grade_items} i
1356              WHERE g.locked = 0 AND g.locktime > 0 AND g.locktime < ? AND g.itemid=i.id AND EXISTS (
1357                 SELECT 'x' FROM {grade_items} c WHERE c.itemtype='course' AND c.needsupdate=0 AND c.courseid=i.courseid)";
1359     // go through all courses that have proper final grades and lock them if needed
1360     $rs = $DB->get_recordset_sql($sql, array($now));
1361     foreach ($rs as $grade) {
1362         $grade_grade = new grade_grade($grade, false);
1363         $grade_grade->locked = $now;
1364         $grade_grade->update('locktime');
1365     }
1366     $rs->close();
1368     //TODO: do not run this cleanup every cron invocation
1369     // cleanup history tables
1370     if (!empty($CFG->gradehistorylifetime)) {  // value in days
1371         $histlifetime = $now - ($CFG->gradehistorylifetime * 3600 * 24);
1372         $tables = array('grade_outcomes_history', 'grade_categories_history', 'grade_items_history', 'grade_grades_history', 'scale_history');
1373         foreach ($tables as $table) {
1374             if ($DB->delete_records_select($table, "timemodified < ?", array($histlifetime))) {
1375                 mtrace("    Deleted old grade history records from '$table'");
1376             }
1377         }
1378     }
1381 /**
1382  * Reset all course grades, refetch from the activities and recalculate
1383  *
1384  * @param int $courseid The course to reset
1385  * @return bool success
1386  */
1387 function grade_course_reset($courseid) {
1389     // no recalculations
1390     grade_force_full_regrading($courseid);
1392     $grade_items = grade_item::fetch_all(array('courseid'=>$courseid));
1393     foreach ($grade_items as $gid=>$grade_item) {
1394         $grade_item->delete_all_grades('reset');
1395     }
1397     //refetch all grades
1398     grade_grab_course_grades($courseid);
1400     // recalculate all grades
1401     grade_regrade_final_grades($courseid);
1402     return true;
1405 /**
1406  * Convert a number to 5 decimal point float, an empty string or a null db compatible format
1407  * (we need this to decide if db value changed)
1408  *
1409  * @param mixed $number The number to convert
1410  * @return mixed float or null
1411  */
1412 function grade_floatval($number) {
1413     if (is_null($number) or $number === '') {
1414         return null;
1415     }
1416     // we must round to 5 digits to get the same precision as in 10,5 db fields
1417     // note: db rounding for 10,5 is different from php round() function
1418     return round($number, 5);
1421 /**
1422  * Compare two float numbers safely. Uses 5 decimals php precision using {@link grade_floatval()}. Nulls accepted too.
1423  * Used for determining if a database update is required
1424  *
1425  * @param float $f1 Float one to compare
1426  * @param float $f2 Float two to compare
1427  * @return bool True if the supplied values are different
1428  */
1429 function grade_floats_different($f1, $f2) {
1430     // note: db rounding for 10,5 is different from php round() function
1431     return (grade_floatval($f1) !== grade_floatval($f2));
1434 /**
1435  * Compare two float numbers safely. Uses 5 decimals php precision using {@link grade_floatval()}
1436  *
1437  * Do not use rounding for 10,5 at the database level as the results may be
1438  * different from php round() function.
1439  *
1440  * @since 2.0
1441  * @param float $f1 Float one to compare
1442  * @param float $f2 Float two to compare
1443  * @return bool True if the values should be considered as the same grades
1444  */
1445 function grade_floats_equal($f1, $f2) {
1446     return (grade_floatval($f1) === grade_floatval($f2));