MDL-55987 gradebook: Claim extra memory when updating final grades
[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  * Return true if the course needs regrading.
330  *
331  * @param int $courseid The course ID
332  * @return bool true if course grades need updating.
333  */
334 function grade_needs_regrade_final_grades($courseid) {
335     $course_item = grade_item::fetch_course_item($courseid);
336     return $course_item->needsupdate;
339 /**
340  * Return true if the regrade process is likely to be time consuming and
341  * will therefore require the progress bar.
342  *
343  * @param int $courseid The course ID
344  * @return bool Whether the regrade process is likely to be time consuming
345  */
346 function grade_needs_regrade_progress_bar($courseid) {
347     global $DB;
348     $grade_items = grade_item::fetch_all(array('courseid' => $courseid));
350     list($sql, $params) = $DB->get_in_or_equal(array_keys($grade_items), SQL_PARAMS_NAMED, 'gi');
351     $gradecount = $DB->count_records_select('grade_grades', 'itemid ' . $sql, $params);
353     // This figure may seem arbitrary, but after analysis it seems that 100 grade_grades can be calculated in ~= 0.5 seconds.
354     // Any longer than this and we want to show the progress bar.
355     return $gradecount > 100;
358 /**
359  * Check whether regarding of final grades is required and, if so, perform the regrade.
360  *
361  * If the regrade is expected to be time consuming (see grade_needs_regrade_progress_bar), then this
362  * function will output the progress bar, and redirect to the current PAGE->url after regrading
363  * completes. Otherwise the regrading will happen immediately and the page will be loaded as per
364  * normal.
365  *
366  * A callback may be specified, which is called if regrading has taken place.
367  * The callback may optionally return a URL which will be redirected to when the progress bar is present.
368  *
369  * @param stdClass $course The course to regrade
370  * @param callable $callback A function to call if regrading took place
371  * @return moodle_url The URL to redirect to if redirecting
372  */
373 function grade_regrade_final_grades_if_required($course, callable $callback = null) {
374     global $PAGE, $OUTPUT;
376     if (!grade_needs_regrade_final_grades($course->id)) {
377         return false;
378     }
380     if (grade_needs_regrade_progress_bar($course->id)) {
381         $PAGE->set_heading($course->fullname);
382         echo $OUTPUT->header();
383         echo $OUTPUT->heading(get_string('recalculatinggrades', 'grades'));
384         $progress = new \core\progress\display(true);
385         $status = grade_regrade_final_grades($course->id, null, null, $progress);
387         // Show regrade errors and set the course to no longer needing regrade (stop endless loop).
388         if (is_array($status)) {
389             foreach ($status as $error) {
390                 $errortext = new \core\output\notification($error, \core\output\notification::NOTIFY_ERROR);
391                 echo $OUTPUT->render($errortext);
392             }
393             $courseitem = grade_item::fetch_course_item($course->id);
394             $courseitem->regrading_finished();
395         }
397         if ($callback) {
398             //
399             $url = call_user_func($callback);
400         }
402         if (empty($url)) {
403             $url = $PAGE->url;
404         }
406         echo $OUTPUT->continue_button($url);
407         echo $OUTPUT->footer();
408         die();
409     } else {
410         $result = grade_regrade_final_grades($course->id);
411         if ($callback) {
412             call_user_func($callback);
413         }
414         return $result;
415     }
418 /**
419  * Returns grading information for given activity, optionally with user grades
420  * Manual, course or category items can not be queried.
421  *
422  * @category grade
423  * @param int    $courseid ID of course
424  * @param string $itemtype Type of grade item. For example, 'mod' or 'block'
425  * @param string $itemmodule More specific then $itemtype. For example, 'forum' or 'quiz'. May be NULL for some item types
426  * @param int    $iteminstance ID of the item module
427  * @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
428  * @return array Array of grade information objects (scaleid, name, grade and locked status, etc.) indexed with itemnumbers
429  */
430 function grade_get_grades($courseid, $itemtype, $itemmodule, $iteminstance, $userid_or_ids=null) {
431     global $CFG;
433     $return = new stdClass();
434     $return->items    = array();
435     $return->outcomes = array();
437     $course_item = grade_item::fetch_course_item($courseid);
438     $needsupdate = array();
439     if ($course_item->needsupdate) {
440         $result = grade_regrade_final_grades($courseid);
441         if ($result !== true) {
442             $needsupdate = array_keys($result);
443         }
444     }
446     if ($grade_items = grade_item::fetch_all(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid))) {
447         foreach ($grade_items as $grade_item) {
448             $decimalpoints = null;
450             if (empty($grade_item->outcomeid)) {
451                 // prepare information about grade item
452                 $item = new stdClass();
453                 $item->id = $grade_item->id;
454                 $item->itemnumber = $grade_item->itemnumber;
455                 $item->itemtype  = $grade_item->itemtype;
456                 $item->itemmodule = $grade_item->itemmodule;
457                 $item->iteminstance = $grade_item->iteminstance;
458                 $item->scaleid    = $grade_item->scaleid;
459                 $item->name       = $grade_item->get_name();
460                 $item->grademin   = $grade_item->grademin;
461                 $item->grademax   = $grade_item->grademax;
462                 $item->gradepass  = $grade_item->gradepass;
463                 $item->locked     = $grade_item->is_locked();
464                 $item->hidden     = $grade_item->is_hidden();
465                 $item->grades     = array();
467                 switch ($grade_item->gradetype) {
468                     case GRADE_TYPE_NONE:
469                         continue;
471                     case GRADE_TYPE_VALUE:
472                         $item->scaleid = 0;
473                         break;
475                     case GRADE_TYPE_TEXT:
476                         $item->scaleid   = 0;
477                         $item->grademin   = 0;
478                         $item->grademax   = 0;
479                         $item->gradepass  = 0;
480                         break;
481                 }
483                 if (empty($userid_or_ids)) {
484                     $userids = array();
486                 } else if (is_array($userid_or_ids)) {
487                     $userids = $userid_or_ids;
489                 } else {
490                     $userids = array($userid_or_ids);
491                 }
493                 if ($userids) {
494                     $grade_grades = grade_grade::fetch_users_grades($grade_item, $userids, true);
495                     foreach ($userids as $userid) {
496                         $grade_grades[$userid]->grade_item =& $grade_item;
498                         $grade = new stdClass();
499                         $grade->grade          = $grade_grades[$userid]->finalgrade;
500                         $grade->locked         = $grade_grades[$userid]->is_locked();
501                         $grade->hidden         = $grade_grades[$userid]->is_hidden();
502                         $grade->overridden     = $grade_grades[$userid]->overridden;
503                         $grade->feedback       = $grade_grades[$userid]->feedback;
504                         $grade->feedbackformat = $grade_grades[$userid]->feedbackformat;
505                         $grade->usermodified   = $grade_grades[$userid]->usermodified;
506                         $grade->datesubmitted  = $grade_grades[$userid]->get_datesubmitted();
507                         $grade->dategraded     = $grade_grades[$userid]->get_dategraded();
509                         // create text representation of grade
510                         if ($grade_item->gradetype == GRADE_TYPE_TEXT or $grade_item->gradetype == GRADE_TYPE_NONE) {
511                             $grade->grade          = null;
512                             $grade->str_grade      = '-';
513                             $grade->str_long_grade = $grade->str_grade;
515                         } else if (in_array($grade_item->id, $needsupdate)) {
516                             $grade->grade          = false;
517                             $grade->str_grade      = get_string('error');
518                             $grade->str_long_grade = $grade->str_grade;
520                         } else if (is_null($grade->grade)) {
521                             $grade->str_grade      = '-';
522                             $grade->str_long_grade = $grade->str_grade;
524                         } else {
525                             $grade->str_grade = grade_format_gradevalue($grade->grade, $grade_item);
526                             if ($grade_item->gradetype == GRADE_TYPE_SCALE or $grade_item->get_displaytype() != GRADE_DISPLAY_TYPE_REAL) {
527                                 $grade->str_long_grade = $grade->str_grade;
528                             } else {
529                                 $a = new stdClass();
530                                 $a->grade = $grade->str_grade;
531                                 $a->max   = grade_format_gradevalue($grade_item->grademax, $grade_item);
532                                 $grade->str_long_grade = get_string('gradelong', 'grades', $a);
533                             }
534                         }
536                         // create html representation of feedback
537                         if (is_null($grade->feedback)) {
538                             $grade->str_feedback = '';
539                         } else {
540                             $grade->str_feedback = format_text($grade->feedback, $grade->feedbackformat);
541                         }
543                         $item->grades[$userid] = $grade;
544                     }
545                 }
546                 $return->items[$grade_item->itemnumber] = $item;
548             } else {
549                 if (!$grade_outcome = grade_outcome::fetch(array('id'=>$grade_item->outcomeid))) {
550                     debugging('Incorect outcomeid found');
551                     continue;
552                 }
554                 // outcome info
555                 $outcome = new stdClass();
556                 $outcome->id = $grade_item->id;
557                 $outcome->itemnumber = $grade_item->itemnumber;
558                 $outcome->itemtype   = $grade_item->itemtype;
559                 $outcome->itemmodule = $grade_item->itemmodule;
560                 $outcome->iteminstance = $grade_item->iteminstance;
561                 $outcome->scaleid    = $grade_outcome->scaleid;
562                 $outcome->name       = $grade_outcome->get_name();
563                 $outcome->locked     = $grade_item->is_locked();
564                 $outcome->hidden     = $grade_item->is_hidden();
566                 if (empty($userid_or_ids)) {
567                     $userids = array();
568                 } else if (is_array($userid_or_ids)) {
569                     $userids = $userid_or_ids;
570                 } else {
571                     $userids = array($userid_or_ids);
572                 }
574                 if ($userids) {
575                     $grade_grades = grade_grade::fetch_users_grades($grade_item, $userids, true);
576                     foreach ($userids as $userid) {
577                         $grade_grades[$userid]->grade_item =& $grade_item;
579                         $grade = new stdClass();
580                         $grade->grade          = $grade_grades[$userid]->finalgrade;
581                         $grade->locked         = $grade_grades[$userid]->is_locked();
582                         $grade->hidden         = $grade_grades[$userid]->is_hidden();
583                         $grade->feedback       = $grade_grades[$userid]->feedback;
584                         $grade->feedbackformat = $grade_grades[$userid]->feedbackformat;
585                         $grade->usermodified   = $grade_grades[$userid]->usermodified;
587                         // create text representation of grade
588                         if (in_array($grade_item->id, $needsupdate)) {
589                             $grade->grade     = false;
590                             $grade->str_grade = get_string('error');
592                         } else if (is_null($grade->grade)) {
593                             $grade->grade = 0;
594                             $grade->str_grade = get_string('nooutcome', 'grades');
596                         } else {
597                             $grade->grade = (int)$grade->grade;
598                             $scale = $grade_item->load_scale();
599                             $grade->str_grade = format_string($scale->scale_items[(int)$grade->grade-1]);
600                         }
602                         // create html representation of feedback
603                         if (is_null($grade->feedback)) {
604                             $grade->str_feedback = '';
605                         } else {
606                             $grade->str_feedback = format_text($grade->feedback, $grade->feedbackformat);
607                         }
609                         $outcome->grades[$userid] = $grade;
610                     }
611                 }
613                 if (isset($return->outcomes[$grade_item->itemnumber])) {
614                     // itemnumber duplicates - lets fix them!
615                     $newnumber = $grade_item->itemnumber + 1;
616                     while(grade_item::fetch(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid, 'itemnumber'=>$newnumber))) {
617                         $newnumber++;
618                     }
619                     $outcome->itemnumber    = $newnumber;
620                     $grade_item->itemnumber = $newnumber;
621                     $grade_item->update('system');
622                 }
624                 $return->outcomes[$grade_item->itemnumber] = $outcome;
626             }
627         }
628     }
630     // sort results using itemnumbers
631     ksort($return->items, SORT_NUMERIC);
632     ksort($return->outcomes, SORT_NUMERIC);
634     return $return;
637 ///////////////////////////////////////////////////////////////////
638 ///// End of public API for communication with modules/blocks /////
639 ///////////////////////////////////////////////////////////////////
643 ///////////////////////////////////////////////////////////////////
644 ///// Internal API: used by gradebook plugins and Moodle core /////
645 ///////////////////////////////////////////////////////////////////
647 /**
648  * Returns a  course gradebook setting
649  *
650  * @param int $courseid
651  * @param string $name of setting, maybe null if reset only
652  * @param string $default value to return if setting is not found
653  * @param bool $resetcache force reset of internal static cache
654  * @return string value of the setting, $default if setting not found, NULL if supplied $name is null
655  */
656 function grade_get_setting($courseid, $name, $default=null, $resetcache=false) {
657     global $DB;
659     static $cache = array();
661     if ($resetcache or !array_key_exists($courseid, $cache)) {
662         $cache[$courseid] = array();
664     } else if (is_null($name)) {
665         return null;
667     } else if (array_key_exists($name, $cache[$courseid])) {
668         return $cache[$courseid][$name];
669     }
671     if (!$data = $DB->get_record('grade_settings', array('courseid'=>$courseid, 'name'=>$name))) {
672         $result = null;
673     } else {
674         $result = $data->value;
675     }
677     if (is_null($result)) {
678         $result = $default;
679     }
681     $cache[$courseid][$name] = $result;
682     return $result;
685 /**
686  * Returns all course gradebook settings as object properties
687  *
688  * @param int $courseid
689  * @return object
690  */
691 function grade_get_settings($courseid) {
692     global $DB;
694      $settings = new stdClass();
695      $settings->id = $courseid;
697     if ($records = $DB->get_records('grade_settings', array('courseid'=>$courseid))) {
698         foreach ($records as $record) {
699             $settings->{$record->name} = $record->value;
700         }
701     }
703     return $settings;
706 /**
707  * Add, update or delete a course gradebook setting
708  *
709  * @param int $courseid The course ID
710  * @param string $name Name of the setting
711  * @param string $value Value of the setting. NULL means delete the setting.
712  */
713 function grade_set_setting($courseid, $name, $value) {
714     global $DB;
716     if (is_null($value)) {
717         $DB->delete_records('grade_settings', array('courseid'=>$courseid, 'name'=>$name));
719     } else if (!$existing = $DB->get_record('grade_settings', array('courseid'=>$courseid, 'name'=>$name))) {
720         $data = new stdClass();
721         $data->courseid = $courseid;
722         $data->name     = $name;
723         $data->value    = $value;
724         $DB->insert_record('grade_settings', $data);
726     } else {
727         $data = new stdClass();
728         $data->id       = $existing->id;
729         $data->value    = $value;
730         $DB->update_record('grade_settings', $data);
731     }
733     grade_get_setting($courseid, null, null, true); // reset the cache
736 /**
737  * Returns string representation of grade value
738  *
739  * @param float $value The grade value
740  * @param object $grade_item Grade item object passed by reference to prevent scale reloading
741  * @param bool $localized use localised decimal separator
742  * @param int $displaytype type of display. For example GRADE_DISPLAY_TYPE_REAL, GRADE_DISPLAY_TYPE_PERCENTAGE, GRADE_DISPLAY_TYPE_LETTER
743  * @param int $decimals The number of decimal places when displaying float values
744  * @return string
745  */
746 function grade_format_gradevalue($value, &$grade_item, $localized=true, $displaytype=null, $decimals=null) {
747     if ($grade_item->gradetype == GRADE_TYPE_NONE or $grade_item->gradetype == GRADE_TYPE_TEXT) {
748         return '';
749     }
751     // no grade yet?
752     if (is_null($value)) {
753         return '-';
754     }
756     if ($grade_item->gradetype != GRADE_TYPE_VALUE and $grade_item->gradetype != GRADE_TYPE_SCALE) {
757         //unknown type??
758         return '';
759     }
761     if (is_null($displaytype)) {
762         $displaytype = $grade_item->get_displaytype();
763     }
765     if (is_null($decimals)) {
766         $decimals = $grade_item->get_decimals();
767     }
769     switch ($displaytype) {
770         case GRADE_DISPLAY_TYPE_REAL:
771             return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized);
773         case GRADE_DISPLAY_TYPE_PERCENTAGE:
774             return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized);
776         case GRADE_DISPLAY_TYPE_LETTER:
777             return grade_format_gradevalue_letter($value, $grade_item);
779         case GRADE_DISPLAY_TYPE_REAL_PERCENTAGE:
780             return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ' (' .
781                     grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ')';
783         case GRADE_DISPLAY_TYPE_REAL_LETTER:
784             return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ' (' .
785                     grade_format_gradevalue_letter($value, $grade_item) . ')';
787         case GRADE_DISPLAY_TYPE_PERCENTAGE_REAL:
788             return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ' (' .
789                     grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ')';
791         case GRADE_DISPLAY_TYPE_LETTER_REAL:
792             return grade_format_gradevalue_letter($value, $grade_item) . ' (' .
793                     grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ')';
795         case GRADE_DISPLAY_TYPE_LETTER_PERCENTAGE:
796             return grade_format_gradevalue_letter($value, $grade_item) . ' (' .
797                     grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ')';
799         case GRADE_DISPLAY_TYPE_PERCENTAGE_LETTER:
800             return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ' (' .
801                     grade_format_gradevalue_letter($value, $grade_item) . ')';
802         default:
803             return '';
804     }
807 /**
808  * Returns a float representation of a grade value
809  *
810  * @param float $value The grade value
811  * @param object $grade_item Grade item object
812  * @param int $decimals The number of decimal places
813  * @param bool $localized use localised decimal separator
814  * @return string
815  */
816 function grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) {
817     if ($grade_item->gradetype == GRADE_TYPE_SCALE) {
818         if (!$scale = $grade_item->load_scale()) {
819             return get_string('error');
820         }
822         $value = $grade_item->bounded_grade($value);
823         return format_string($scale->scale_items[$value-1]);
825     } else {
826         return format_float($value, $decimals, $localized);
827     }
830 /**
831  * Returns a percentage representation of a grade value
832  *
833  * @param float $value The grade value
834  * @param object $grade_item Grade item object
835  * @param int $decimals The number of decimal places
836  * @param bool $localized use localised decimal separator
837  * @return string
838  */
839 function grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) {
840     $min = $grade_item->grademin;
841     $max = $grade_item->grademax;
842     if ($min == $max) {
843         return '';
844     }
845     $value = $grade_item->bounded_grade($value);
846     $percentage = (($value-$min)*100)/($max-$min);
847     return format_float($percentage, $decimals, $localized).' %';
850 /**
851  * Returns a letter grade representation of a grade value
852  * The array of grade letters used is produced by {@link grade_get_letters()} using the course context
853  *
854  * @param float $value The grade value
855  * @param object $grade_item Grade item object
856  * @return string
857  */
858 function grade_format_gradevalue_letter($value, $grade_item) {
859     global $CFG;
860     $context = context_course::instance($grade_item->courseid, IGNORE_MISSING);
861     if (!$letters = grade_get_letters($context)) {
862         return ''; // no letters??
863     }
865     if (is_null($value)) {
866         return '-';
867     }
869     $value = grade_grade::standardise_score($value, $grade_item->grademin, $grade_item->grademax, 0, 100);
870     $value = bounded_number(0, $value, 100); // just in case
872     $gradebookcalculationsfreeze = 'gradebook_calculations_freeze_' . $grade_item->courseid;
874     foreach ($letters as $boundary => $letter) {
875         if (property_exists($CFG, $gradebookcalculationsfreeze) && (int)$CFG->{$gradebookcalculationsfreeze} <= 20160518) {
876             // Do nothing.
877         } else {
878             // The boundary is a percentage out of 100 so use 0 as the min and 100 as the max.
879             $boundary = grade_grade::standardise_score($boundary, 0, 100, 0, 100);
880         }
881         if ($value >= $boundary) {
882             return format_string($letter);
883         }
884     }
885     return '-'; // no match? maybe '' would be more correct
889 /**
890  * Returns grade options for gradebook grade category menu
891  *
892  * @param int $courseid The course ID
893  * @param bool $includenew Include option for new category at array index -1
894  * @return array of grade categories in course
895  */
896 function grade_get_categories_menu($courseid, $includenew=false) {
897     $result = array();
898     if (!$categories = grade_category::fetch_all(array('courseid'=>$courseid))) {
899         //make sure course category exists
900         if (!grade_category::fetch_course_category($courseid)) {
901             debugging('Can not create course grade category!');
902             return $result;
903         }
904         $categories = grade_category::fetch_all(array('courseid'=>$courseid));
905     }
906     foreach ($categories as $key=>$category) {
907         if ($category->is_course_category()) {
908             $result[$category->id] = get_string('uncategorised', 'grades');
909             unset($categories[$key]);
910         }
911     }
912     if ($includenew) {
913         $result[-1] = get_string('newcategory', 'grades');
914     }
915     $cats = array();
916     foreach ($categories as $category) {
917         $cats[$category->id] = $category->get_name();
918     }
919     core_collator::asort($cats);
921     return ($result+$cats);
924 /**
925  * Returns the array of grade letters to be used in the supplied context
926  *
927  * @param object $context Context object or null for defaults
928  * @return array of grade_boundary (minimum) => letter_string
929  */
930 function grade_get_letters($context=null) {
931     global $DB;
933     if (empty($context)) {
934         //default grading letters
935         return array('93'=>'A', '90'=>'A-', '87'=>'B+', '83'=>'B', '80'=>'B-', '77'=>'C+', '73'=>'C', '70'=>'C-', '67'=>'D+', '60'=>'D', '0'=>'F');
936     }
938     static $cache = array();
940     if (array_key_exists($context->id, $cache)) {
941         return $cache[$context->id];
942     }
944     if (count($cache) > 100) {
945         $cache = array(); // cache size limit
946     }
948     $letters = array();
950     $contexts = $context->get_parent_context_ids();
951     array_unshift($contexts, $context->id);
953     foreach ($contexts as $ctxid) {
954         if ($records = $DB->get_records('grade_letters', array('contextid'=>$ctxid), 'lowerboundary DESC')) {
955             foreach ($records as $record) {
956                 $letters[$record->lowerboundary] = $record->letter;
957             }
958         }
960         if (!empty($letters)) {
961             $cache[$context->id] = $letters;
962             return $letters;
963         }
964     }
966     $letters = grade_get_letters(null);
967     $cache[$context->id] = $letters;
968     return $letters;
972 /**
973  * Verify new value of grade item idnumber. Checks for uniqueness of new ID numbers. Old ID numbers are kept intact.
974  *
975  * @param string $idnumber string (with magic quotes)
976  * @param int $courseid ID numbers are course unique only
977  * @param grade_item $grade_item The grade item this idnumber is associated with
978  * @param stdClass $cm used for course module idnumbers and items attached to modules
979  * @return bool true means idnumber ok
980  */
981 function grade_verify_idnumber($idnumber, $courseid, $grade_item=null, $cm=null) {
982     global $DB;
984     if ($idnumber == '') {
985         //we allow empty idnumbers
986         return true;
987     }
989     // keep existing even when not unique
990     if ($cm and $cm->idnumber == $idnumber) {
991         if ($grade_item and $grade_item->itemnumber != 0) {
992             // grade item with itemnumber > 0 can't have the same idnumber as the main
993             // itemnumber 0 which is synced with course_modules
994             return false;
995         }
996         return true;
997     } else if ($grade_item and $grade_item->idnumber == $idnumber) {
998         return true;
999     }
1001     if ($DB->record_exists('course_modules', array('course'=>$courseid, 'idnumber'=>$idnumber))) {
1002         return false;
1003     }
1005     if ($DB->record_exists('grade_items', array('courseid'=>$courseid, 'idnumber'=>$idnumber))) {
1006         return false;
1007     }
1009     return true;
1012 /**
1013  * Force final grade recalculation in all course items
1014  *
1015  * @param int $courseid The course ID to recalculate
1016  */
1017 function grade_force_full_regrading($courseid) {
1018     global $DB;
1019     $DB->set_field('grade_items', 'needsupdate', 1, array('courseid'=>$courseid));
1022 /**
1023  * Forces regrading of all site grades. Used when changing site setings
1024  */
1025 function grade_force_site_regrading() {
1026     global $CFG, $DB;
1027     $DB->set_field('grade_items', 'needsupdate', 1);
1030 /**
1031  * Recover a user's grades from grade_grades_history
1032  * @param int $userid the user ID whose grades we want to recover
1033  * @param int $courseid the relevant course
1034  * @return bool true if successful or false if there was an error or no grades could be recovered
1035  */
1036 function grade_recover_history_grades($userid, $courseid) {
1037     global $CFG, $DB;
1039     if ($CFG->disablegradehistory) {
1040         debugging('Attempting to recover grades when grade history is disabled.');
1041         return false;
1042     }
1044     //Were grades recovered? Flag to return.
1045     $recoveredgrades = false;
1047     //Check the user is enrolled in this course
1048     //Dont bother checking if they have a gradeable role. They may get one later so recover
1049     //whatever grades they have now just in case.
1050     $course_context = context_course::instance($courseid);
1051     if (!is_enrolled($course_context, $userid)) {
1052         debugging('Attempting to recover the grades of a user who is deleted or not enrolled. Skipping recover.');
1053         return false;
1054     }
1056     //Check for existing grades for this user in this course
1057     //Recovering grades when the user already has grades can lead to duplicate indexes and bad data
1058     //In the future we could move the existing grades to the history table then recover the grades from before then
1059     $sql = "SELECT gg.id
1060               FROM {grade_grades} gg
1061               JOIN {grade_items} gi ON gi.id = gg.itemid
1062              WHERE gi.courseid = :courseid AND gg.userid = :userid";
1063     $params = array('userid' => $userid, 'courseid' => $courseid);
1064     if ($DB->record_exists_sql($sql, $params)) {
1065         debugging('Attempting to recover the grades of a user who already has grades. Skipping recover.');
1066         return false;
1067     } else {
1068         //Retrieve the user's old grades
1069         //have history ID as first column to guarantee we a unique first column
1070         $sql = "SELECT h.id, gi.itemtype, gi.itemmodule, gi.iteminstance as iteminstance, gi.itemnumber, h.source, h.itemid, h.userid, h.rawgrade, h.rawgrademax,
1071                        h.rawgrademin, h.rawscaleid, h.usermodified, h.finalgrade, h.hidden, h.locked, h.locktime, h.exported, h.overridden, h.excluded, h.feedback,
1072                        h.feedbackformat, h.information, h.informationformat, h.timemodified, itemcreated.tm AS timecreated
1073                   FROM {grade_grades_history} h
1074                   JOIN (SELECT itemid, MAX(id) AS id
1075                           FROM {grade_grades_history}
1076                          WHERE userid = :userid1
1077                       GROUP BY itemid) maxquery ON h.id = maxquery.id AND h.itemid = maxquery.itemid
1078                   JOIN {grade_items} gi ON gi.id = h.itemid
1079                   JOIN (SELECT itemid, MAX(timemodified) AS tm
1080                           FROM {grade_grades_history}
1081                          WHERE userid = :userid2 AND action = :insertaction
1082                       GROUP BY itemid) itemcreated ON itemcreated.itemid = h.itemid
1083                  WHERE gi.courseid = :courseid";
1084         $params = array('userid1' => $userid, 'userid2' => $userid , 'insertaction' => GRADE_HISTORY_INSERT, 'courseid' => $courseid);
1085         $oldgrades = $DB->get_records_sql($sql, $params);
1087         //now move the old grades to the grade_grades table
1088         foreach ($oldgrades as $oldgrade) {
1089             unset($oldgrade->id);
1091             $grade = new grade_grade($oldgrade, false);//2nd arg false as dont want to try and retrieve a record from the DB
1092             $grade->insert($oldgrade->source);
1094             //dont include default empty grades created when activities are created
1095             if (!is_null($oldgrade->finalgrade) || !is_null($oldgrade->feedback)) {
1096                 $recoveredgrades = true;
1097             }
1098         }
1099     }
1101     //Some activities require manual grade synching (moving grades from the activity into the gradebook)
1102     //If the student was deleted when synching was done they may have grades in the activity that haven't been moved across
1103     grade_grab_course_grades($courseid, null, $userid);
1105     return $recoveredgrades;
1108 /**
1109  * Updates all final grades in course.
1110  *
1111  * @param int $courseid The course ID
1112  * @param int $userid If specified try to do a quick regrading of the grades of this user only
1113  * @param object $updated_item Optional grade item to be marked for regrading
1114  * @param \core\progress\base $progress If provided, will be used to update progress on this long operation.
1115  * @return bool true if ok, array of errors if problems found. Grade item id => error message
1116  */
1117 function grade_regrade_final_grades($courseid, $userid=null, $updated_item=null, $progress=null) {
1118     // This may take a very long time and extra memory.
1119     \core_php_time_limit::raise();
1120     raise_memory_limit(MEMORY_EXTRA);
1122     $course_item = grade_item::fetch_course_item($courseid);
1124     if ($progress == null) {
1125         $progress = new \core\progress\none();
1126     }
1128     if ($userid) {
1129         // one raw grade updated for one user
1130         if (empty($updated_item)) {
1131             print_error("cannotbenull", 'debug', '', "updated_item");
1132         }
1133         if ($course_item->needsupdate) {
1134             $updated_item->force_regrading();
1135             return array($course_item->id =>'Can not do fast regrading after updating of raw grades');
1136         }
1138     } else {
1139         if (!$course_item->needsupdate) {
1140             // nothing to do :-)
1141             return true;
1142         }
1143     }
1145     // Categories might have to run some processing before we fetch the grade items.
1146     // This gives them a final opportunity to update and mark their children to be updated.
1147     // We need to work on the children categories up to the parent ones, so that, for instance,
1148     // if a category total is updated it will be reflected in the parent category.
1149     $cats = grade_category::fetch_all(array('courseid' => $courseid));
1150     $flatcattree = array();
1151     foreach ($cats as $cat) {
1152         if (!isset($flatcattree[$cat->depth])) {
1153             $flatcattree[$cat->depth] = array();
1154         }
1155         $flatcattree[$cat->depth][] = $cat;
1156     }
1157     krsort($flatcattree);
1158     foreach ($flatcattree as $depth => $cats) {
1159         foreach ($cats as $cat) {
1160             $cat->pre_regrade_final_grades();
1161         }
1162     }
1164     $progresstotal = 0;
1165     $progresscurrent = 0;
1167     $grade_items = grade_item::fetch_all(array('courseid'=>$courseid));
1168     $depends_on = array();
1170     foreach ($grade_items as $gid=>$gitem) {
1171         if ((!empty($updated_item) and $updated_item->id == $gid) ||
1172                 $gitem->is_course_item() || $gitem->is_category_item() || $gitem->is_calculated()) {
1173             $grade_items[$gid]->needsupdate = 1;
1174         }
1176         // We load all dependencies of these items later we can discard some grade_items based on this.
1177         if ($grade_items[$gid]->needsupdate) {
1178             $depends_on[$gid] = $grade_items[$gid]->depends_on();
1179             $progresstotal++;
1180         }
1181     }
1183     $progress->start_progress('regrade_course', $progresstotal);
1185     $errors = array();
1186     $finalids = array();
1187     $updatedids = array();
1188     $gids     = array_keys($grade_items);
1189     $failed = 0;
1191     while (count($finalids) < count($gids)) { // work until all grades are final or error found
1192         $count = 0;
1193         foreach ($gids as $gid) {
1194             if (in_array($gid, $finalids)) {
1195                 continue; // already final
1196             }
1198             if (!$grade_items[$gid]->needsupdate) {
1199                 $finalids[] = $gid; // we can make it final - does not need update
1200                 continue;
1201             }
1202             $thisprogress = $progresstotal;
1203             foreach ($grade_items as $item) {
1204                 if ($item->needsupdate) {
1205                     $thisprogress--;
1206                 }
1207             }
1208             // Clip between $progresscurrent and $progresstotal.
1209             $thisprogress = max(min($thisprogress, $progresstotal), $progresscurrent);
1210             $progress->progress($thisprogress);
1211             $progresscurrent = $thisprogress;
1213             foreach ($depends_on[$gid] as $did) {
1214                 if (!in_array($did, $finalids)) {
1215                     // This item depends on something that is not yet in finals array.
1216                     continue 2;
1217                 }
1218             }
1220             // If this grade item has no dependancy with any updated item at all, then remove it from being recalculated.
1222             // When we get here, all of this grade item's decendents are marked as final so they would be marked as updated too
1223             // if they would have been regraded. We don't need to regrade items which dependants (not only the direct ones
1224             // but any dependant in the cascade) have not been updated.
1226             // If $updated_item was specified we discard the grade items that do not depend on it or on any grade item that
1227             // depend on $updated_item.
1229             // Here we check to see if the direct decendants are marked as updated.
1230             if (!empty($updated_item) && $gid != $updated_item->id && !in_array($updated_item->id, $depends_on[$gid])) {
1232                 // We need to ensure that none of this item's dependencies have been updated.
1233                 // If we find that one of the direct decendants of this grade item is marked as updated then this
1234                 // grade item needs to be recalculated and marked as updated.
1235                 // Being marked as updated is done further down in the code.
1237                 $updateddependencies = false;
1238                 foreach ($depends_on[$gid] as $dependency) {
1239                     if (in_array($dependency, $updatedids)) {
1240                         $updateddependencies = true;
1241                         break;
1242                     }
1243                 }
1244                 if ($updateddependencies === false) {
1245                     // If no direct descendants are marked as updated, then we don't need to update this grade item. We then mark it
1246                     // as final.
1248                     $finalids[] = $gid;
1249                     continue;
1250                 }
1251             }
1253             // Let's update, calculate or aggregate.
1254             $result = $grade_items[$gid]->regrade_final_grades($userid);
1256             if ($result === true) {
1258                 // We should only update the database if we regraded all users.
1259                 if (empty($userid)) {
1260                     $grade_items[$gid]->regrading_finished();
1261                     // Do the locktime item locking.
1262                     $grade_items[$gid]->check_locktime();
1263                 } else {
1264                     $grade_items[$gid]->needsupdate = 0;
1265                 }
1266                 $count++;
1267                 $finalids[] = $gid;
1268                 $updatedids[] = $gid;
1270             } else {
1271                 $grade_items[$gid]->force_regrading();
1272                 $errors[$gid] = $result;
1273             }
1274         }
1276         if ($count == 0) {
1277             $failed++;
1278         } else {
1279             $failed = 0;
1280         }
1282         if ($failed > 1) {
1283             foreach($gids as $gid) {
1284                 if (in_array($gid, $finalids)) {
1285                     continue; // this one is ok
1286                 }
1287                 $grade_items[$gid]->force_regrading();
1288                 $errors[$grade_items[$gid]->id] = get_string('errorcalculationbroken', 'grades');
1289             }
1290             break; // Found error.
1291         }
1292     }
1293     $progress->end_progress();
1295     if (count($errors) == 0) {
1296         if (empty($userid)) {
1297             // do the locktime locking of grades, but only when doing full regrading
1298             grade_grade::check_locktime_all($gids);
1299         }
1300         return true;
1301     } else {
1302         return $errors;
1303     }
1306 /**
1307  * Refetches grade data from course activities
1308  *
1309  * @param int $courseid The course ID
1310  * @param string $modname Limit the grade fetch to a single module type. For example 'forum'
1311  * @param int $userid limit the grade fetch to a single user
1312  */
1313 function grade_grab_course_grades($courseid, $modname=null, $userid=0) {
1314     global $CFG, $DB;
1316     if ($modname) {
1317         $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname
1318                   FROM {".$modname."} a, {course_modules} cm, {modules} m
1319                  WHERE m.name=:modname AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=:courseid";
1320         $params = array('modname'=>$modname, 'courseid'=>$courseid);
1322         if ($modinstances = $DB->get_records_sql($sql, $params)) {
1323             foreach ($modinstances as $modinstance) {
1324                 grade_update_mod_grades($modinstance, $userid);
1325             }
1326         }
1327         return;
1328     }
1330     if (!$mods = core_component::get_plugin_list('mod') ) {
1331         print_error('nomodules', 'debug');
1332     }
1334     foreach ($mods as $mod => $fullmod) {
1335         if ($mod == 'NEWMODULE') {   // Someone has unzipped the template, ignore it
1336             continue;
1337         }
1339         // include the module lib once
1340         if (file_exists($fullmod.'/lib.php')) {
1341             // get all instance of the activity
1342             $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname
1343                       FROM {".$mod."} a, {course_modules} cm, {modules} m
1344                      WHERE m.name=:mod AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=:courseid";
1345             $params = array('mod'=>$mod, 'courseid'=>$courseid);
1347             if ($modinstances = $DB->get_records_sql($sql, $params)) {
1348                 foreach ($modinstances as $modinstance) {
1349                     grade_update_mod_grades($modinstance, $userid);
1350                 }
1351             }
1352         }
1353     }
1356 /**
1357  * Force full update of module grades in central gradebook
1358  *
1359  * @param object $modinstance Module object with extra cmidnumber and modname property
1360  * @param int $userid Optional user ID if limiting the update to a single user
1361  * @return bool True if success
1362  */
1363 function grade_update_mod_grades($modinstance, $userid=0) {
1364     global $CFG, $DB;
1366     $fullmod = $CFG->dirroot.'/mod/'.$modinstance->modname;
1367     if (!file_exists($fullmod.'/lib.php')) {
1368         debugging('missing lib.php file in module ' . $modinstance->modname);
1369         return false;
1370     }
1371     include_once($fullmod.'/lib.php');
1373     $updateitemfunc   = $modinstance->modname.'_grade_item_update';
1374     $updategradesfunc = $modinstance->modname.'_update_grades';
1376     if (function_exists($updategradesfunc) and function_exists($updateitemfunc)) {
1377         //new grading supported, force updating of grades
1378         $updateitemfunc($modinstance);
1379         $updategradesfunc($modinstance, $userid);
1381     } else {
1382         // Module does not support grading?
1383     }
1385     return true;
1388 /**
1389  * Remove grade letters for given context
1390  *
1391  * @param context $context The context
1392  * @param bool $showfeedback If true a success notification will be displayed
1393  */
1394 function remove_grade_letters($context, $showfeedback) {
1395     global $DB, $OUTPUT;
1397     $strdeleted = get_string('deleted');
1399     $DB->delete_records('grade_letters', array('contextid'=>$context->id));
1400     if ($showfeedback) {
1401         echo $OUTPUT->notification($strdeleted.' - '.get_string('letters', 'grades'), 'notifysuccess');
1402     }
1405 /**
1406  * Remove all grade related course data
1407  * Grade history is kept
1408  *
1409  * @param int $courseid The course ID
1410  * @param bool $showfeedback If true success notifications will be displayed
1411  */
1412 function remove_course_grades($courseid, $showfeedback) {
1413     global $DB, $OUTPUT;
1415     $fs = get_file_storage();
1416     $strdeleted = get_string('deleted');
1418     $course_category = grade_category::fetch_course_category($courseid);
1419     $course_category->delete('coursedelete');
1420     $fs->delete_area_files(context_course::instance($courseid)->id, 'grade', 'feedback');
1421     if ($showfeedback) {
1422         echo $OUTPUT->notification($strdeleted.' - '.get_string('grades', 'grades').', '.get_string('items', 'grades').', '.get_string('categories', 'grades'), 'notifysuccess');
1423     }
1425     if ($outcomes = grade_outcome::fetch_all(array('courseid'=>$courseid))) {
1426         foreach ($outcomes as $outcome) {
1427             $outcome->delete('coursedelete');
1428         }
1429     }
1430     $DB->delete_records('grade_outcomes_courses', array('courseid'=>$courseid));
1431     if ($showfeedback) {
1432         echo $OUTPUT->notification($strdeleted.' - '.get_string('outcomes', 'grades'), 'notifysuccess');
1433     }
1435     if ($scales = grade_scale::fetch_all(array('courseid'=>$courseid))) {
1436         foreach ($scales as $scale) {
1437             $scale->delete('coursedelete');
1438         }
1439     }
1440     if ($showfeedback) {
1441         echo $OUTPUT->notification($strdeleted.' - '.get_string('scales'), 'notifysuccess');
1442     }
1444     $DB->delete_records('grade_settings', array('courseid'=>$courseid));
1445     if ($showfeedback) {
1446         echo $OUTPUT->notification($strdeleted.' - '.get_string('settings', 'grades'), 'notifysuccess');
1447     }
1450 /**
1451  * Called when course category is deleted
1452  * Cleans the gradebook of associated data
1453  *
1454  * @param int $categoryid The course category id
1455  * @param int $newparentid If empty everything is deleted. Otherwise the ID of the category where content moved
1456  * @param bool $showfeedback print feedback
1457  */
1458 function grade_course_category_delete($categoryid, $newparentid, $showfeedback) {
1459     global $DB;
1461     $context = context_coursecat::instance($categoryid);
1462     $DB->delete_records('grade_letters', array('contextid'=>$context->id));
1465 /**
1466  * Does gradebook cleanup when a module is uninstalled
1467  * Deletes all associated grade items
1468  *
1469  * @param string $modname The grade item module name to remove. For example 'forum'
1470  */
1471 function grade_uninstalled_module($modname) {
1472     global $CFG, $DB;
1474     $sql = "SELECT *
1475               FROM {grade_items}
1476              WHERE itemtype='mod' AND itemmodule=?";
1478     // go all items for this module and delete them including the grades
1479     $rs = $DB->get_recordset_sql($sql, array($modname));
1480     foreach ($rs as $item) {
1481         $grade_item = new grade_item($item, false);
1482         $grade_item->delete('moduninstall');
1483     }
1484     $rs->close();
1487 /**
1488  * Deletes all of a user's grade data from gradebook
1489  *
1490  * @param int $userid The user whose grade data should be deleted
1491  */
1492 function grade_user_delete($userid) {
1493     if ($grades = grade_grade::fetch_all(array('userid'=>$userid))) {
1494         foreach ($grades as $grade) {
1495             $grade->delete('userdelete');
1496         }
1497     }
1500 /**
1501  * Purge course data when user unenrolls from a course
1502  *
1503  * @param int $courseid The ID of the course the user has unenrolled from
1504  * @param int $userid The ID of the user unenrolling
1505  */
1506 function grade_user_unenrol($courseid, $userid) {
1507     if ($items = grade_item::fetch_all(array('courseid'=>$courseid))) {
1508         foreach ($items as $item) {
1509             if ($grades = grade_grade::fetch_all(array('userid'=>$userid, 'itemid'=>$item->id))) {
1510                 foreach ($grades as $grade) {
1511                     $grade->delete('userdelete');
1512                 }
1513             }
1514         }
1515     }
1518 /**
1519  * Grading cron job. Performs background clean up on the gradebook
1520  */
1521 function grade_cron() {
1522     global $CFG, $DB;
1524     $now = time();
1526     $sql = "SELECT i.*
1527               FROM {grade_items} i
1528              WHERE i.locked = 0 AND i.locktime > 0 AND i.locktime < ? AND EXISTS (
1529                 SELECT 'x' FROM {grade_items} c WHERE c.itemtype='course' AND c.needsupdate=0 AND c.courseid=i.courseid)";
1531     // go through all courses that have proper final grades and lock them if needed
1532     $rs = $DB->get_recordset_sql($sql, array($now));
1533     foreach ($rs as $item) {
1534         $grade_item = new grade_item($item, false);
1535         $grade_item->locked = $now;
1536         $grade_item->update('locktime');
1537     }
1538     $rs->close();
1540     $grade_inst = new grade_grade();
1541     $fields = 'g.'.implode(',g.', $grade_inst->required_fields);
1543     $sql = "SELECT $fields
1544               FROM {grade_grades} g, {grade_items} i
1545              WHERE g.locked = 0 AND g.locktime > 0 AND g.locktime < ? AND g.itemid=i.id AND EXISTS (
1546                 SELECT 'x' FROM {grade_items} c WHERE c.itemtype='course' AND c.needsupdate=0 AND c.courseid=i.courseid)";
1548     // go through all courses that have proper final grades and lock them if needed
1549     $rs = $DB->get_recordset_sql($sql, array($now));
1550     foreach ($rs as $grade) {
1551         $grade_grade = new grade_grade($grade, false);
1552         $grade_grade->locked = $now;
1553         $grade_grade->update('locktime');
1554     }
1555     $rs->close();
1557     //TODO: do not run this cleanup every cron invocation
1558     // cleanup history tables
1559     if (!empty($CFG->gradehistorylifetime)) {  // value in days
1560         $histlifetime = $now - ($CFG->gradehistorylifetime * 3600 * 24);
1561         $tables = array('grade_outcomes_history', 'grade_categories_history', 'grade_items_history', 'grade_grades_history', 'scale_history');
1562         foreach ($tables as $table) {
1563             if ($DB->delete_records_select($table, "timemodified < ?", array($histlifetime))) {
1564                 mtrace("    Deleted old grade history records from '$table'");
1565             }
1566         }
1567     }
1570 /**
1571  * Reset all course grades, refetch from the activities and recalculate
1572  *
1573  * @param int $courseid The course to reset
1574  * @return bool success
1575  */
1576 function grade_course_reset($courseid) {
1578     // no recalculations
1579     grade_force_full_regrading($courseid);
1581     $grade_items = grade_item::fetch_all(array('courseid'=>$courseid));
1582     foreach ($grade_items as $gid=>$grade_item) {
1583         $grade_item->delete_all_grades('reset');
1584     }
1586     //refetch all grades
1587     grade_grab_course_grades($courseid);
1589     // recalculate all grades
1590     grade_regrade_final_grades($courseid);
1591     return true;
1594 /**
1595  * Convert a number to 5 decimal point float, an empty string or a null db compatible format
1596  * (we need this to decide if db value changed)
1597  *
1598  * @param mixed $number The number to convert
1599  * @return mixed float or null
1600  */
1601 function grade_floatval($number) {
1602     if (is_null($number) or $number === '') {
1603         return null;
1604     }
1605     // we must round to 5 digits to get the same precision as in 10,5 db fields
1606     // note: db rounding for 10,5 is different from php round() function
1607     return round($number, 5);
1610 /**
1611  * Compare two float numbers safely. Uses 5 decimals php precision using {@link grade_floatval()}. Nulls accepted too.
1612  * Used for determining if a database update is required
1613  *
1614  * @param float $f1 Float one to compare
1615  * @param float $f2 Float two to compare
1616  * @return bool True if the supplied values are different
1617  */
1618 function grade_floats_different($f1, $f2) {
1619     // note: db rounding for 10,5 is different from php round() function
1620     return (grade_floatval($f1) !== grade_floatval($f2));
1623 /**
1624  * Compare two float numbers safely. Uses 5 decimals php precision using {@link grade_floatval()}
1625  *
1626  * Do not use rounding for 10,5 at the database level as the results may be
1627  * different from php round() function.
1628  *
1629  * @since Moodle 2.0
1630  * @param float $f1 Float one to compare
1631  * @param float $f2 Float two to compare
1632  * @return bool True if the values should be considered as the same grades
1633  */
1634 function grade_floats_equal($f1, $f2) {
1635     return (grade_floatval($f1) === grade_floatval($f2));