7cda902f28a8bd1c3dff505a3ee34f5c430f9cea
[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 one or more activities, 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 = 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);
353         }
354     }
356     $params = array('courseid'=>$courseid);
357     if (!empty($itemtype)) {
358         $params['itemtype'] = $itemtype;
359     }
360     if (!empty($itemmodule)) {
361         $params['itemmodule'] = $itemmodule;
362     }
363     if (!empty($iteminstance)) {
364         $params['iteminstance'] = $iteminstance;
365     }
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;
401                 }
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);
411                 }
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);
453                             }
454                         }
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);
461                         }
463                         $item->grades[$userid] = $grade;
464                     }
465                 }
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;
472                 }
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);
492                 }
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]);
520                         }
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);
527                         }
529                         $outcome->grades[$userid] = $grade;
530                     }
531                 }
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++;
538                     }
539                     $outcome->itemnumber    = $newnumber;
540                     $grade_item->itemnumber = $newnumber;
541                     $grade_item->update('system');
542                 }
544                 $return->outcomes[$grade_item->itemnumber] = $outcome;
546             }
547         }
548     }
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 ///////////////////////////////////////////////////////////////////
567 /**
568  * Returns a  course gradebook setting
569  *
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
575  */
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];
589     }
591     if (!$data = $DB->get_record('grade_settings', array('courseid'=>$courseid, 'name'=>$name))) {
592         $result = null;
593     } else {
594         $result = $data->value;
595     }
597     if (is_null($result)) {
598         $result = $default;
599     }
601     $cache[$courseid][$name] = $result;
602     return $result;
605 /**
606  * Returns all course gradebook settings as object properties
607  *
608  * @param int $courseid
609  * @return object
610  */
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;
620         }
621     }
623     return $settings;
626 /**
627  * Add, update or delete a course gradebook setting
628  *
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.
632  */
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);
651     }
653     grade_get_setting($courseid, null, null, true); // reset the cache
656 /**
657  * Returns string representation of grade value
658  *
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
665  */
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 '';
669     }
671     // no grade yet?
672     if (is_null($value)) {
673         return '-';
674     }
676     if ($grade_item->gradetype != GRADE_TYPE_VALUE and $grade_item->gradetype != GRADE_TYPE_SCALE) {
677         //unknown type??
678         return '';
679     }
681     if (is_null($displaytype)) {
682         $displaytype = $grade_item->get_displaytype();
683     }
685     if (is_null($decimals)) {
686         $decimals = $grade_item->get_decimals();
687     }
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 '';
724     }
727 /**
728  * Returns a float representation of a grade value
729  *
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
735  */
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');
740         }
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);
747     }
750 /**
751  * Returns a percentage representation of a grade value
752  *
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
758  */
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 '';
764     }
765     $value = $grade_item->bounded_grade($value);
766     $percentage = (($value-$min)*100)/($max-$min);
767     return format_float($percentage, $decimals, $localized).' %';
770 /**
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
773  *
774  * @param float $value The grade value
775  * @param object $grade_item Grade item object
776  * @return string
777  */
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??
782     }
784     if (is_null($value)) {
785         return '-';
786     }
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);
793         }
794     }
795     return '-'; // no match? maybe '' would be more correct
799 /**
800  * Returns grade options for gradebook grade category menu
801  *
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
805  */
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;
813         }
814         $categories = grade_category::fetch_all(array('courseid'=>$courseid));
815     }
816     foreach ($categories as $key=>$category) {
817         if ($category->is_course_category()) {
818             $result[$category->id] = get_string('uncategorised', 'grades');
819             unset($categories[$key]);
820         }
821     }
822     if ($includenew) {
823         $result[-1] = get_string('newcategory', 'grades');
824     }
825     $cats = array();
826     foreach ($categories as $category) {
827         $cats[$category->id] = $category->get_name();
828     }
829     core_collator::asort($cats);
831     return ($result+$cats);
834 /**
835  * Returns the array of grade letters to be used in the supplied context
836  *
837  * @param object $context Context object or null for defaults
838  * @return array of grade_boundary (minimum) => letter_string
839  */
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');
846     }
848     static $cache = array();
850     if (array_key_exists($context->id, $cache)) {
851         return $cache[$context->id];
852     }
854     if (count($cache) > 100) {
855         $cache = array(); // cache size limit
856     }
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;
867             }
868         }
870         if (!empty($letters)) {
871             $cache[$context->id] = $letters;
872             return $letters;
873         }
874     }
876     $letters = grade_get_letters(null);
877     $cache[$context->id] = $letters;
878     return $letters;
882 /**
883  * Verify new value of grade item idnumber. Checks for uniqueness of new ID numbers. Old ID numbers are kept intact.
884  *
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
890  */
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;
897     }
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;
905         }
906         return true;
907     } else if ($grade_item and $grade_item->idnumber == $idnumber) {
908         return true;
909     }
911     if ($DB->record_exists('course_modules', array('course'=>$courseid, 'idnumber'=>$idnumber))) {
912         return false;
913     }
915     if ($DB->record_exists('grade_items', array('courseid'=>$courseid, 'idnumber'=>$idnumber))) {
916         return false;
917     }
919     return true;
922 /**
923  * Force final grade recalculation in all course items
924  *
925  * @param int $courseid The course ID to recalculate
926  */
927 function grade_force_full_regrading($courseid) {
928     global $DB;
929     $DB->set_field('grade_items', 'needsupdate', 1, array('courseid'=>$courseid));
932 /**
933  * Forces regrading of all site grades. Used when changing site setings
934  */
935 function grade_force_site_regrading() {
936     global $CFG, $DB;
937     $DB->set_field('grade_items', 'needsupdate', 1);
940 /**
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
945  */
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;
952     }
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;
964     }
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;
1007             }
1008         }
1009     }
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;
1018 /**
1019  * Updates all final grades in course.
1020  *
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
1025  */
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");
1034         }
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');
1038         }
1040     } else {
1041         if (!$course_item->needsupdate) {
1042             // nothing to do :-)
1043             return true;
1044         }
1045     }
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;
1058         }
1060         // construct depends_on lookup array
1061         $depends_on[$gid] = $grade_items[$gid]->depends_on();
1062     }
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
1074             }
1076             if (!$grade_items[$gid]->needsupdate) {
1077                 $finalids[] = $gid; // we can make it final - does not need update
1078                 continue;
1079             }
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
1086                 }
1087             }
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;
1102                 }
1103             }
1104         }
1106         if ($count == 0) {
1107             $failed++;
1108         } else {
1109             $failed = 0;
1110         }
1112         if ($failed > 1) {
1113             foreach($gids as $gid) {
1114                 if (in_array($gid, $finalids)) {
1115                     continue; // this one is ok
1116                 }
1117                 $grade_items[$gid]->force_regrading();
1118                 $errors[$grade_items[$gid]->id] = get_string('errorcalculationbroken', 'grades');
1119             }
1120             break; // Found error.
1121         }
1122     }
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);
1128         }
1129         return true;
1130     } else {
1131         return $errors;
1132     }
1135 /**
1136  * Refetches grade data from course activities
1137  *
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
1141  */
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);
1154             }
1155         }
1156         return;
1157     }
1159     if (!$mods = core_component::get_plugin_list('mod') ) {
1160         print_error('nomodules', 'debug');
1161     }
1163     foreach ($mods as $mod => $fullmod) {
1164         if ($mod == 'NEWMODULE') {   // Someone has unzipped the template, ignore it
1165             continue;
1166         }
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);
1179                 }
1180             }
1181         }
1182     }
1185 /**
1186  * Force full update of module grades in central gradebook
1187  *
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
1191  */
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;
1199     }
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?
1212     }
1214     return true;
1217 /**
1218  * Remove grade letters for given context
1219  *
1220  * @param context $context The context
1221  * @param bool $showfeedback If true a success notification will be displayed
1222  */
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');
1231     }
1234 /**
1235  * Remove all grade related course data
1236  * Grade history is kept
1237  *
1238  * @param int $courseid The course ID
1239  * @param bool $showfeedback If true success notifications will be displayed
1240  */
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');
1252     }
1254     if ($outcomes = grade_outcome::fetch_all(array('courseid'=>$courseid))) {
1255         foreach ($outcomes as $outcome) {
1256             $outcome->delete('coursedelete');
1257         }
1258     }
1259     $DB->delete_records('grade_outcomes_courses', array('courseid'=>$courseid));
1260     if ($showfeedback) {
1261         echo $OUTPUT->notification($strdeleted.' - '.get_string('outcomes', 'grades'), 'notifysuccess');
1262     }
1264     if ($scales = grade_scale::fetch_all(array('courseid'=>$courseid))) {
1265         foreach ($scales as $scale) {
1266             $scale->delete('coursedelete');
1267         }
1268     }
1269     if ($showfeedback) {
1270         echo $OUTPUT->notification($strdeleted.' - '.get_string('scales'), 'notifysuccess');
1271     }
1273     $DB->delete_records('grade_settings', array('courseid'=>$courseid));
1274     if ($showfeedback) {
1275         echo $OUTPUT->notification($strdeleted.' - '.get_string('settings', 'grades'), 'notifysuccess');
1276     }
1279 /**
1280  * Called when course category is deleted
1281  * Cleans the gradebook of associated data
1282  *
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
1286  */
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));
1294 /**
1295  * Does gradebook cleanup when a module is uninstalled
1296  * Deletes all associated grade items
1297  *
1298  * @param string $modname The grade item module name to remove. For example 'forum'
1299  */
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');
1312     }
1313     $rs->close();
1316 /**
1317  * Deletes all of a user's grade data from gradebook
1318  *
1319  * @param int $userid The user whose grade data should be deleted
1320  */
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');
1325         }
1326     }
1329 /**
1330  * Purge course data when user unenrolls from a course
1331  *
1332  * @param int $courseid The ID of the course the user has unenrolled from
1333  * @param int $userid The ID of the user unenrolling
1334  */
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');
1341                 }
1342             }
1343         }
1344     }
1347 /**
1348  * Grading cron job. Performs background clean up on the gradebook
1349  */
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');
1366     }
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');
1383     }
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'");
1394             }
1395         }
1396     }
1399 /**
1400  * Reset all course grades, refetch from the activities and recalculate
1401  *
1402  * @param int $courseid The course to reset
1403  * @return bool success
1404  */
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');
1413     }
1415     //refetch all grades
1416     grade_grab_course_grades($courseid);
1418     // recalculate all grades
1419     grade_regrade_final_grades($courseid);
1420     return true;
1423 /**
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)
1426  *
1427  * @param mixed $number The number to convert
1428  * @return mixed float or null
1429  */
1430 function grade_floatval($number) {
1431     if (is_null($number) or $number === '') {
1432         return null;
1433     }
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);
1439 /**
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
1442  *
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
1446  */
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));
1452 /**
1453  * Compare two float numbers safely. Uses 5 decimals php precision using {@link grade_floatval()}
1454  *
1455  * Do not use rounding for 10,5 at the database level as the results may be
1456  * different from php round() function.
1457  *
1458  * @since 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
1462  */
1463 function grade_floats_equal($f1, $f2) {
1464     return (grade_floatval($f1) === grade_floatval($f2));