MDL-64609 gradebook: Prevent infinite loop in regrading
[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         $feedbackfiles = [];
256         $usermodified   = $USER->id;
257         $datesubmitted  = null;
258         $dategraded     = null;
260         if (array_key_exists('rawgrade', $grade)) {
261             $rawgrade = $grade['rawgrade'];
262         }
264         if (array_key_exists('feedback', $grade)) {
265             $feedback = $grade['feedback'];
266         }
268         if (array_key_exists('feedbackformat', $grade)) {
269             $feedbackformat = $grade['feedbackformat'];
270         }
272         if (array_key_exists('feedbackfiles', $grade)) {
273             $feedbackfiles = $grade['feedbackfiles'];
274         }
276         if (array_key_exists('usermodified', $grade)) {
277             $usermodified = $grade['usermodified'];
278         }
280         if (array_key_exists('datesubmitted', $grade)) {
281             $datesubmitted = $grade['datesubmitted'];
282         }
284         if (array_key_exists('dategraded', $grade)) {
285             $dategraded = $grade['dategraded'];
286         }
288         // update or insert the grade
289         if (!$grade_item->update_raw_grade($userid, $rawgrade, $source, $feedback, $feedbackformat, $usermodified,
290                 $dategraded, $datesubmitted, $grade_grade, $feedbackfiles)) {
291             $failed = true;
292         }
293     }
295     if ($rs) {
296         $rs->close();
297     }
299     if (!$failed) {
300         return GRADE_UPDATE_OK;
301     } else {
302         return GRADE_UPDATE_FAILED;
303     }
306 /**
307  * Updates a user's outcomes. Manual outcomes can not be updated.
308  *
309  * @category grade
310  * @param string $source Source of the grade such as 'mod/assignment'
311  * @param int    $courseid ID of course
312  * @param string $itemtype Type of grade item. For example, 'mod' or 'block'
313  * @param string $itemmodule More specific then $itemtype. For example, 'forum' or 'quiz'. May be NULL for some item types
314  * @param int    $iteminstance Instance ID of graded item. For example the forum ID.
315  * @param int    $userid ID of the graded user
316  * @param array  $data Array consisting of grade item itemnumber ({@link grade_update()}) => outcomegrade
317  * @return bool returns true if grade items were found and updated successfully
318  */
319 function grade_update_outcomes($source, $courseid, $itemtype, $itemmodule, $iteminstance, $userid, $data) {
320     if ($items = grade_item::fetch_all(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid))) {
321         $result = true;
322         foreach ($items as $item) {
323             if (!array_key_exists($item->itemnumber, $data)) {
324                 continue;
325             }
326             $grade = $data[$item->itemnumber] < 1 ? null : $data[$item->itemnumber];
327             $result = ($item->update_final_grade($userid, $grade, $source) && $result);
328         }
329         return $result;
330     }
331     return false; //grade items not found
334 /**
335  * Return true if the course needs regrading.
336  *
337  * @param int $courseid The course ID
338  * @return bool true if course grades need updating.
339  */
340 function grade_needs_regrade_final_grades($courseid) {
341     $course_item = grade_item::fetch_course_item($courseid);
342     return $course_item->needsupdate;
345 /**
346  * Return true if the regrade process is likely to be time consuming and
347  * will therefore require the progress bar.
348  *
349  * @param int $courseid The course ID
350  * @return bool Whether the regrade process is likely to be time consuming
351  */
352 function grade_needs_regrade_progress_bar($courseid) {
353     global $DB;
354     $grade_items = grade_item::fetch_all(array('courseid' => $courseid));
356     list($sql, $params) = $DB->get_in_or_equal(array_keys($grade_items), SQL_PARAMS_NAMED, 'gi');
357     $gradecount = $DB->count_records_select('grade_grades', 'itemid ' . $sql, $params);
359     // This figure may seem arbitrary, but after analysis it seems that 100 grade_grades can be calculated in ~= 0.5 seconds.
360     // Any longer than this and we want to show the progress bar.
361     return $gradecount > 100;
364 /**
365  * Check whether regarding of final grades is required and, if so, perform the regrade.
366  *
367  * If the regrade is expected to be time consuming (see grade_needs_regrade_progress_bar), then this
368  * function will output the progress bar, and redirect to the current PAGE->url after regrading
369  * completes. Otherwise the regrading will happen immediately and the page will be loaded as per
370  * normal.
371  *
372  * A callback may be specified, which is called if regrading has taken place.
373  * The callback may optionally return a URL which will be redirected to when the progress bar is present.
374  *
375  * @param stdClass $course The course to regrade
376  * @param callable $callback A function to call if regrading took place
377  * @return moodle_url The URL to redirect to if redirecting
378  */
379 function grade_regrade_final_grades_if_required($course, callable $callback = null) {
380     global $PAGE, $OUTPUT;
382     if (!grade_needs_regrade_final_grades($course->id)) {
383         return false;
384     }
386     if (grade_needs_regrade_progress_bar($course->id)) {
387         $PAGE->set_heading($course->fullname);
388         echo $OUTPUT->header();
389         echo $OUTPUT->heading(get_string('recalculatinggrades', 'grades'));
390         $progress = new \core\progress\display(true);
391         $status = grade_regrade_final_grades($course->id, null, null, $progress);
393         // Show regrade errors and set the course to no longer needing regrade (stop endless loop).
394         if (is_array($status)) {
395             foreach ($status as $error) {
396                 $errortext = new \core\output\notification($error, \core\output\notification::NOTIFY_ERROR);
397                 echo $OUTPUT->render($errortext);
398             }
399             $courseitem = grade_item::fetch_course_item($course->id);
400             $courseitem->regrading_finished();
401         }
403         if ($callback) {
404             //
405             $url = call_user_func($callback);
406         }
408         if (empty($url)) {
409             $url = $PAGE->url;
410         }
412         echo $OUTPUT->continue_button($url);
413         echo $OUTPUT->footer();
414         die();
415     } else {
416         $result = grade_regrade_final_grades($course->id);
417         if ($callback) {
418             call_user_func($callback);
419         }
420         return $result;
421     }
424 /**
425  * Returns grading information for given activity, optionally with user grades
426  * Manual, course or category items can not be queried.
427  *
428  * @category grade
429  * @param int    $courseid ID of course
430  * @param string $itemtype Type of grade item. For example, 'mod' or 'block'
431  * @param string $itemmodule More specific then $itemtype. For example, 'forum' or 'quiz'. May be NULL for some item types
432  * @param int    $iteminstance ID of the item module
433  * @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
434  * @return array Array of grade information objects (scaleid, name, grade and locked status, etc.) indexed with itemnumbers
435  */
436 function grade_get_grades($courseid, $itemtype, $itemmodule, $iteminstance, $userid_or_ids=null) {
437     global $CFG;
439     $return = new stdClass();
440     $return->items    = array();
441     $return->outcomes = array();
443     $course_item = grade_item::fetch_course_item($courseid);
444     $needsupdate = array();
445     if ($course_item->needsupdate) {
446         $result = grade_regrade_final_grades($courseid);
447         if ($result !== true) {
448             $needsupdate = array_keys($result);
449         }
450     }
452     if ($grade_items = grade_item::fetch_all(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid))) {
453         foreach ($grade_items as $grade_item) {
454             $decimalpoints = null;
456             if (empty($grade_item->outcomeid)) {
457                 // prepare information about grade item
458                 $item = new stdClass();
459                 $item->id = $grade_item->id;
460                 $item->itemnumber = $grade_item->itemnumber;
461                 $item->itemtype  = $grade_item->itemtype;
462                 $item->itemmodule = $grade_item->itemmodule;
463                 $item->iteminstance = $grade_item->iteminstance;
464                 $item->scaleid    = $grade_item->scaleid;
465                 $item->name       = $grade_item->get_name();
466                 $item->grademin   = $grade_item->grademin;
467                 $item->grademax   = $grade_item->grademax;
468                 $item->gradepass  = $grade_item->gradepass;
469                 $item->locked     = $grade_item->is_locked();
470                 $item->hidden     = $grade_item->is_hidden();
471                 $item->grades     = array();
473                 switch ($grade_item->gradetype) {
474                     case GRADE_TYPE_NONE:
475                         break;
477                     case GRADE_TYPE_VALUE:
478                         $item->scaleid = 0;
479                         break;
481                     case GRADE_TYPE_TEXT:
482                         $item->scaleid   = 0;
483                         $item->grademin   = 0;
484                         $item->grademax   = 0;
485                         $item->gradepass  = 0;
486                         break;
487                 }
489                 if (empty($userid_or_ids)) {
490                     $userids = array();
492                 } else if (is_array($userid_or_ids)) {
493                     $userids = $userid_or_ids;
495                 } else {
496                     $userids = array($userid_or_ids);
497                 }
499                 if ($userids) {
500                     $grade_grades = grade_grade::fetch_users_grades($grade_item, $userids, true);
501                     foreach ($userids as $userid) {
502                         $grade_grades[$userid]->grade_item =& $grade_item;
504                         $grade = new stdClass();
505                         $grade->grade          = $grade_grades[$userid]->finalgrade;
506                         $grade->locked         = $grade_grades[$userid]->is_locked();
507                         $grade->hidden         = $grade_grades[$userid]->is_hidden();
508                         $grade->overridden     = $grade_grades[$userid]->overridden;
509                         $grade->feedback       = $grade_grades[$userid]->feedback;
510                         $grade->feedbackformat = $grade_grades[$userid]->feedbackformat;
511                         $grade->usermodified   = $grade_grades[$userid]->usermodified;
512                         $grade->datesubmitted  = $grade_grades[$userid]->get_datesubmitted();
513                         $grade->dategraded     = $grade_grades[$userid]->get_dategraded();
515                         // create text representation of grade
516                         if ($grade_item->gradetype == GRADE_TYPE_TEXT or $grade_item->gradetype == GRADE_TYPE_NONE) {
517                             $grade->grade          = null;
518                             $grade->str_grade      = '-';
519                             $grade->str_long_grade = $grade->str_grade;
521                         } else if (in_array($grade_item->id, $needsupdate)) {
522                             $grade->grade          = false;
523                             $grade->str_grade      = get_string('error');
524                             $grade->str_long_grade = $grade->str_grade;
526                         } else if (is_null($grade->grade)) {
527                             $grade->str_grade      = '-';
528                             $grade->str_long_grade = $grade->str_grade;
530                         } else {
531                             $grade->str_grade = grade_format_gradevalue($grade->grade, $grade_item);
532                             if ($grade_item->gradetype == GRADE_TYPE_SCALE or $grade_item->get_displaytype() != GRADE_DISPLAY_TYPE_REAL) {
533                                 $grade->str_long_grade = $grade->str_grade;
534                             } else {
535                                 $a = new stdClass();
536                                 $a->grade = $grade->str_grade;
537                                 $a->max   = grade_format_gradevalue($grade_item->grademax, $grade_item);
538                                 $grade->str_long_grade = get_string('gradelong', 'grades', $a);
539                             }
540                         }
542                         // create html representation of feedback
543                         if (is_null($grade->feedback)) {
544                             $grade->str_feedback = '';
545                         } else {
546                             $feedback = file_rewrite_pluginfile_urls(
547                                 $grade->feedback,
548                                 'pluginfile.php',
549                                 $grade_grades[$userid]->get_context()->id,
550                                 GRADE_FILE_COMPONENT,
551                                 GRADE_FEEDBACK_FILEAREA,
552                                 $grade_grades[$userid]->id
553                             );
555                             $grade->str_feedback = format_text($feedback, $grade->feedbackformat,
556                                 ['context' => $grade_grades[$userid]->get_context()]);
557                         }
559                         $item->grades[$userid] = $grade;
560                     }
561                 }
562                 $return->items[$grade_item->itemnumber] = $item;
564             } else {
565                 if (!$grade_outcome = grade_outcome::fetch(array('id'=>$grade_item->outcomeid))) {
566                     debugging('Incorect outcomeid found');
567                     continue;
568                 }
570                 // outcome info
571                 $outcome = new stdClass();
572                 $outcome->id = $grade_item->id;
573                 $outcome->itemnumber = $grade_item->itemnumber;
574                 $outcome->itemtype   = $grade_item->itemtype;
575                 $outcome->itemmodule = $grade_item->itemmodule;
576                 $outcome->iteminstance = $grade_item->iteminstance;
577                 $outcome->scaleid    = $grade_outcome->scaleid;
578                 $outcome->name       = $grade_outcome->get_name();
579                 $outcome->locked     = $grade_item->is_locked();
580                 $outcome->hidden     = $grade_item->is_hidden();
582                 if (empty($userid_or_ids)) {
583                     $userids = array();
584                 } else if (is_array($userid_or_ids)) {
585                     $userids = $userid_or_ids;
586                 } else {
587                     $userids = array($userid_or_ids);
588                 }
590                 if ($userids) {
591                     $grade_grades = grade_grade::fetch_users_grades($grade_item, $userids, true);
592                     foreach ($userids as $userid) {
593                         $grade_grades[$userid]->grade_item =& $grade_item;
595                         $grade = new stdClass();
596                         $grade->grade          = $grade_grades[$userid]->finalgrade;
597                         $grade->locked         = $grade_grades[$userid]->is_locked();
598                         $grade->hidden         = $grade_grades[$userid]->is_hidden();
599                         $grade->feedback       = $grade_grades[$userid]->feedback;
600                         $grade->feedbackformat = $grade_grades[$userid]->feedbackformat;
601                         $grade->usermodified   = $grade_grades[$userid]->usermodified;
602                         $grade->datesubmitted  = $grade_grades[$userid]->get_datesubmitted();
603                         $grade->dategraded     = $grade_grades[$userid]->get_dategraded();
605                         // create text representation of grade
606                         if (in_array($grade_item->id, $needsupdate)) {
607                             $grade->grade     = false;
608                             $grade->str_grade = get_string('error');
610                         } else if (is_null($grade->grade)) {
611                             $grade->grade = 0;
612                             $grade->str_grade = get_string('nooutcome', 'grades');
614                         } else {
615                             $grade->grade = (int)$grade->grade;
616                             $scale = $grade_item->load_scale();
617                             $grade->str_grade = format_string($scale->scale_items[(int)$grade->grade-1]);
618                         }
620                         // create html representation of feedback
621                         if (is_null($grade->feedback)) {
622                             $grade->str_feedback = '';
623                         } else {
624                             $grade->str_feedback = format_text($grade->feedback, $grade->feedbackformat);
625                         }
627                         $outcome->grades[$userid] = $grade;
628                     }
629                 }
631                 if (isset($return->outcomes[$grade_item->itemnumber])) {
632                     // itemnumber duplicates - lets fix them!
633                     $newnumber = $grade_item->itemnumber + 1;
634                     while(grade_item::fetch(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid, 'itemnumber'=>$newnumber))) {
635                         $newnumber++;
636                     }
637                     $outcome->itemnumber    = $newnumber;
638                     $grade_item->itemnumber = $newnumber;
639                     $grade_item->update('system');
640                 }
642                 $return->outcomes[$grade_item->itemnumber] = $outcome;
644             }
645         }
646     }
648     // sort results using itemnumbers
649     ksort($return->items, SORT_NUMERIC);
650     ksort($return->outcomes, SORT_NUMERIC);
652     return $return;
655 ///////////////////////////////////////////////////////////////////
656 ///// End of public API for communication with modules/blocks /////
657 ///////////////////////////////////////////////////////////////////
661 ///////////////////////////////////////////////////////////////////
662 ///// Internal API: used by gradebook plugins and Moodle core /////
663 ///////////////////////////////////////////////////////////////////
665 /**
666  * Returns a  course gradebook setting
667  *
668  * @param int $courseid
669  * @param string $name of setting, maybe null if reset only
670  * @param string $default value to return if setting is not found
671  * @param bool $resetcache force reset of internal static cache
672  * @return string value of the setting, $default if setting not found, NULL if supplied $name is null
673  */
674 function grade_get_setting($courseid, $name, $default=null, $resetcache=false) {
675     global $DB;
677     static $cache = array();
679     if ($resetcache or !array_key_exists($courseid, $cache)) {
680         $cache[$courseid] = array();
682     } else if (is_null($name)) {
683         return null;
685     } else if (array_key_exists($name, $cache[$courseid])) {
686         return $cache[$courseid][$name];
687     }
689     if (!$data = $DB->get_record('grade_settings', array('courseid'=>$courseid, 'name'=>$name))) {
690         $result = null;
691     } else {
692         $result = $data->value;
693     }
695     if (is_null($result)) {
696         $result = $default;
697     }
699     $cache[$courseid][$name] = $result;
700     return $result;
703 /**
704  * Returns all course gradebook settings as object properties
705  *
706  * @param int $courseid
707  * @return object
708  */
709 function grade_get_settings($courseid) {
710     global $DB;
712      $settings = new stdClass();
713      $settings->id = $courseid;
715     if ($records = $DB->get_records('grade_settings', array('courseid'=>$courseid))) {
716         foreach ($records as $record) {
717             $settings->{$record->name} = $record->value;
718         }
719     }
721     return $settings;
724 /**
725  * Add, update or delete a course gradebook setting
726  *
727  * @param int $courseid The course ID
728  * @param string $name Name of the setting
729  * @param string $value Value of the setting. NULL means delete the setting.
730  */
731 function grade_set_setting($courseid, $name, $value) {
732     global $DB;
734     if (is_null($value)) {
735         $DB->delete_records('grade_settings', array('courseid'=>$courseid, 'name'=>$name));
737     } else if (!$existing = $DB->get_record('grade_settings', array('courseid'=>$courseid, 'name'=>$name))) {
738         $data = new stdClass();
739         $data->courseid = $courseid;
740         $data->name     = $name;
741         $data->value    = $value;
742         $DB->insert_record('grade_settings', $data);
744     } else {
745         $data = new stdClass();
746         $data->id       = $existing->id;
747         $data->value    = $value;
748         $DB->update_record('grade_settings', $data);
749     }
751     grade_get_setting($courseid, null, null, true); // reset the cache
754 /**
755  * Returns string representation of grade value
756  *
757  * @param float $value The grade value
758  * @param object $grade_item Grade item object passed by reference to prevent scale reloading
759  * @param bool $localized use localised decimal separator
760  * @param int $displaytype type of display. For example GRADE_DISPLAY_TYPE_REAL, GRADE_DISPLAY_TYPE_PERCENTAGE, GRADE_DISPLAY_TYPE_LETTER
761  * @param int $decimals The number of decimal places when displaying float values
762  * @return string
763  */
764 function grade_format_gradevalue($value, &$grade_item, $localized=true, $displaytype=null, $decimals=null) {
765     if ($grade_item->gradetype == GRADE_TYPE_NONE or $grade_item->gradetype == GRADE_TYPE_TEXT) {
766         return '';
767     }
769     // no grade yet?
770     if (is_null($value)) {
771         return '-';
772     }
774     if ($grade_item->gradetype != GRADE_TYPE_VALUE and $grade_item->gradetype != GRADE_TYPE_SCALE) {
775         //unknown type??
776         return '';
777     }
779     if (is_null($displaytype)) {
780         $displaytype = $grade_item->get_displaytype();
781     }
783     if (is_null($decimals)) {
784         $decimals = $grade_item->get_decimals();
785     }
787     switch ($displaytype) {
788         case GRADE_DISPLAY_TYPE_REAL:
789             return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized);
791         case GRADE_DISPLAY_TYPE_PERCENTAGE:
792             return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized);
794         case GRADE_DISPLAY_TYPE_LETTER:
795             return grade_format_gradevalue_letter($value, $grade_item);
797         case GRADE_DISPLAY_TYPE_REAL_PERCENTAGE:
798             return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ' (' .
799                     grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ')';
801         case GRADE_DISPLAY_TYPE_REAL_LETTER:
802             return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ' (' .
803                     grade_format_gradevalue_letter($value, $grade_item) . ')';
805         case GRADE_DISPLAY_TYPE_PERCENTAGE_REAL:
806             return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ' (' .
807                     grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ')';
809         case GRADE_DISPLAY_TYPE_LETTER_REAL:
810             return grade_format_gradevalue_letter($value, $grade_item) . ' (' .
811                     grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ')';
813         case GRADE_DISPLAY_TYPE_LETTER_PERCENTAGE:
814             return grade_format_gradevalue_letter($value, $grade_item) . ' (' .
815                     grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ')';
817         case GRADE_DISPLAY_TYPE_PERCENTAGE_LETTER:
818             return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ' (' .
819                     grade_format_gradevalue_letter($value, $grade_item) . ')';
820         default:
821             return '';
822     }
825 /**
826  * Returns a float representation of a grade value
827  *
828  * @param float $value The grade value
829  * @param object $grade_item Grade item object
830  * @param int $decimals The number of decimal places
831  * @param bool $localized use localised decimal separator
832  * @return string
833  */
834 function grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) {
835     if ($grade_item->gradetype == GRADE_TYPE_SCALE) {
836         if (!$scale = $grade_item->load_scale()) {
837             return get_string('error');
838         }
840         $value = $grade_item->bounded_grade($value);
841         return format_string($scale->scale_items[$value-1]);
843     } else {
844         return format_float($value, $decimals, $localized);
845     }
848 /**
849  * Returns a percentage representation of a grade value
850  *
851  * @param float $value The grade value
852  * @param object $grade_item Grade item object
853  * @param int $decimals The number of decimal places
854  * @param bool $localized use localised decimal separator
855  * @return string
856  */
857 function grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) {
858     $min = $grade_item->grademin;
859     $max = $grade_item->grademax;
860     if ($min == $max) {
861         return '';
862     }
863     $value = $grade_item->bounded_grade($value);
864     $percentage = (($value-$min)*100)/($max-$min);
865     return format_float($percentage, $decimals, $localized).' %';
868 /**
869  * Returns a letter grade representation of a grade value
870  * The array of grade letters used is produced by {@link grade_get_letters()} using the course context
871  *
872  * @param float $value The grade value
873  * @param object $grade_item Grade item object
874  * @return string
875  */
876 function grade_format_gradevalue_letter($value, $grade_item) {
877     global $CFG;
878     $context = context_course::instance($grade_item->courseid, IGNORE_MISSING);
879     if (!$letters = grade_get_letters($context)) {
880         return ''; // no letters??
881     }
883     if (is_null($value)) {
884         return '-';
885     }
887     $value = grade_grade::standardise_score($value, $grade_item->grademin, $grade_item->grademax, 0, 100);
888     $value = bounded_number(0, $value, 100); // just in case
890     $gradebookcalculationsfreeze = 'gradebook_calculations_freeze_' . $grade_item->courseid;
892     foreach ($letters as $boundary => $letter) {
893         if (property_exists($CFG, $gradebookcalculationsfreeze) && (int)$CFG->{$gradebookcalculationsfreeze} <= 20160518) {
894             // Do nothing.
895         } else {
896             // The boundary is a percentage out of 100 so use 0 as the min and 100 as the max.
897             $boundary = grade_grade::standardise_score($boundary, 0, 100, 0, 100);
898         }
899         if ($value >= $boundary) {
900             return format_string($letter);
901         }
902     }
903     return '-'; // no match? maybe '' would be more correct
907 /**
908  * Returns grade options for gradebook grade category menu
909  *
910  * @param int $courseid The course ID
911  * @param bool $includenew Include option for new category at array index -1
912  * @return array of grade categories in course
913  */
914 function grade_get_categories_menu($courseid, $includenew=false) {
915     $result = array();
916     if (!$categories = grade_category::fetch_all(array('courseid'=>$courseid))) {
917         //make sure course category exists
918         if (!grade_category::fetch_course_category($courseid)) {
919             debugging('Can not create course grade category!');
920             return $result;
921         }
922         $categories = grade_category::fetch_all(array('courseid'=>$courseid));
923     }
924     foreach ($categories as $key=>$category) {
925         if ($category->is_course_category()) {
926             $result[$category->id] = get_string('uncategorised', 'grades');
927             unset($categories[$key]);
928         }
929     }
930     if ($includenew) {
931         $result[-1] = get_string('newcategory', 'grades');
932     }
933     $cats = array();
934     foreach ($categories as $category) {
935         $cats[$category->id] = $category->get_name();
936     }
937     core_collator::asort($cats);
939     return ($result+$cats);
942 /**
943  * Returns the array of grade letters to be used in the supplied context
944  *
945  * @param object $context Context object or null for defaults
946  * @return array of grade_boundary (minimum) => letter_string
947  */
948 function grade_get_letters($context=null) {
949     global $DB;
951     if (empty($context)) {
952         //default grading letters
953         return array('93'=>'A', '90'=>'A-', '87'=>'B+', '83'=>'B', '80'=>'B-', '77'=>'C+', '73'=>'C', '70'=>'C-', '67'=>'D+', '60'=>'D', '0'=>'F');
954     }
956     static $cache = array();
958     if (array_key_exists($context->id, $cache)) {
959         return $cache[$context->id];
960     }
962     if (count($cache) > 100) {
963         $cache = array(); // cache size limit
964     }
966     $letters = array();
968     $contexts = $context->get_parent_context_ids();
969     array_unshift($contexts, $context->id);
971     foreach ($contexts as $ctxid) {
972         if ($records = $DB->get_records('grade_letters', array('contextid'=>$ctxid), 'lowerboundary DESC')) {
973             foreach ($records as $record) {
974                 $letters[$record->lowerboundary] = $record->letter;
975             }
976         }
978         if (!empty($letters)) {
979             $cache[$context->id] = $letters;
980             return $letters;
981         }
982     }
984     $letters = grade_get_letters(null);
985     $cache[$context->id] = $letters;
986     return $letters;
990 /**
991  * Verify new value of grade item idnumber. Checks for uniqueness of new ID numbers. Old ID numbers are kept intact.
992  *
993  * @param string $idnumber string (with magic quotes)
994  * @param int $courseid ID numbers are course unique only
995  * @param grade_item $grade_item The grade item this idnumber is associated with
996  * @param stdClass $cm used for course module idnumbers and items attached to modules
997  * @return bool true means idnumber ok
998  */
999 function grade_verify_idnumber($idnumber, $courseid, $grade_item=null, $cm=null) {
1000     global $DB;
1002     if ($idnumber == '') {
1003         //we allow empty idnumbers
1004         return true;
1005     }
1007     // keep existing even when not unique
1008     if ($cm and $cm->idnumber == $idnumber) {
1009         if ($grade_item and $grade_item->itemnumber != 0) {
1010             // grade item with itemnumber > 0 can't have the same idnumber as the main
1011             // itemnumber 0 which is synced with course_modules
1012             return false;
1013         }
1014         return true;
1015     } else if ($grade_item and $grade_item->idnumber == $idnumber) {
1016         return true;
1017     }
1019     if ($DB->record_exists('course_modules', array('course'=>$courseid, 'idnumber'=>$idnumber))) {
1020         return false;
1021     }
1023     if ($DB->record_exists('grade_items', array('courseid'=>$courseid, 'idnumber'=>$idnumber))) {
1024         return false;
1025     }
1027     return true;
1030 /**
1031  * Force final grade recalculation in all course items
1032  *
1033  * @param int $courseid The course ID to recalculate
1034  */
1035 function grade_force_full_regrading($courseid) {
1036     global $DB;
1037     $DB->set_field('grade_items', 'needsupdate', 1, array('courseid'=>$courseid));
1040 /**
1041  * Forces regrading of all site grades. Used when changing site setings
1042  */
1043 function grade_force_site_regrading() {
1044     global $CFG, $DB;
1045     $DB->set_field('grade_items', 'needsupdate', 1);
1048 /**
1049  * Recover a user's grades from grade_grades_history
1050  * @param int $userid the user ID whose grades we want to recover
1051  * @param int $courseid the relevant course
1052  * @return bool true if successful or false if there was an error or no grades could be recovered
1053  */
1054 function grade_recover_history_grades($userid, $courseid) {
1055     global $CFG, $DB;
1057     if ($CFG->disablegradehistory) {
1058         debugging('Attempting to recover grades when grade history is disabled.');
1059         return false;
1060     }
1062     //Were grades recovered? Flag to return.
1063     $recoveredgrades = false;
1065     //Check the user is enrolled in this course
1066     //Dont bother checking if they have a gradeable role. They may get one later so recover
1067     //whatever grades they have now just in case.
1068     $course_context = context_course::instance($courseid);
1069     if (!is_enrolled($course_context, $userid)) {
1070         debugging('Attempting to recover the grades of a user who is deleted or not enrolled. Skipping recover.');
1071         return false;
1072     }
1074     //Check for existing grades for this user in this course
1075     //Recovering grades when the user already has grades can lead to duplicate indexes and bad data
1076     //In the future we could move the existing grades to the history table then recover the grades from before then
1077     $sql = "SELECT gg.id
1078               FROM {grade_grades} gg
1079               JOIN {grade_items} gi ON gi.id = gg.itemid
1080              WHERE gi.courseid = :courseid AND gg.userid = :userid";
1081     $params = array('userid' => $userid, 'courseid' => $courseid);
1082     if ($DB->record_exists_sql($sql, $params)) {
1083         debugging('Attempting to recover the grades of a user who already has grades. Skipping recover.');
1084         return false;
1085     } else {
1086         //Retrieve the user's old grades
1087         //have history ID as first column to guarantee we a unique first column
1088         $sql = "SELECT h.id, gi.itemtype, gi.itemmodule, gi.iteminstance as iteminstance, gi.itemnumber, h.source, h.itemid, h.userid, h.rawgrade, h.rawgrademax,
1089                        h.rawgrademin, h.rawscaleid, h.usermodified, h.finalgrade, h.hidden, h.locked, h.locktime, h.exported, h.overridden, h.excluded, h.feedback,
1090                        h.feedbackformat, h.information, h.informationformat, h.timemodified, itemcreated.tm AS timecreated
1091                   FROM {grade_grades_history} h
1092                   JOIN (SELECT itemid, MAX(id) AS id
1093                           FROM {grade_grades_history}
1094                          WHERE userid = :userid1
1095                       GROUP BY itemid) maxquery ON h.id = maxquery.id AND h.itemid = maxquery.itemid
1096                   JOIN {grade_items} gi ON gi.id = h.itemid
1097                   JOIN (SELECT itemid, MAX(timemodified) AS tm
1098                           FROM {grade_grades_history}
1099                          WHERE userid = :userid2 AND action = :insertaction
1100                       GROUP BY itemid) itemcreated ON itemcreated.itemid = h.itemid
1101                  WHERE gi.courseid = :courseid";
1102         $params = array('userid1' => $userid, 'userid2' => $userid , 'insertaction' => GRADE_HISTORY_INSERT, 'courseid' => $courseid);
1103         $oldgrades = $DB->get_records_sql($sql, $params);
1105         //now move the old grades to the grade_grades table
1106         foreach ($oldgrades as $oldgrade) {
1107             unset($oldgrade->id);
1109             $grade = new grade_grade($oldgrade, false);//2nd arg false as dont want to try and retrieve a record from the DB
1110             $grade->insert($oldgrade->source);
1112             //dont include default empty grades created when activities are created
1113             if (!is_null($oldgrade->finalgrade) || !is_null($oldgrade->feedback)) {
1114                 $recoveredgrades = true;
1115             }
1116         }
1117     }
1119     //Some activities require manual grade synching (moving grades from the activity into the gradebook)
1120     //If the student was deleted when synching was done they may have grades in the activity that haven't been moved across
1121     grade_grab_course_grades($courseid, null, $userid);
1123     return $recoveredgrades;
1126 /**
1127  * Updates all final grades in course.
1128  *
1129  * @param int $courseid The course ID
1130  * @param int $userid If specified try to do a quick regrading of the grades of this user only
1131  * @param object $updated_item Optional grade item to be marked for regrading. It is required if $userid is set.
1132  * @param \core\progress\base $progress If provided, will be used to update progress on this long operation.
1133  * @return bool true if ok, array of errors if problems found. Grade item id => error message
1134  */
1135 function grade_regrade_final_grades($courseid, $userid=null, $updated_item=null, $progress=null) {
1136     // This may take a very long time and extra memory.
1137     \core_php_time_limit::raise();
1138     raise_memory_limit(MEMORY_EXTRA);
1140     $course_item = grade_item::fetch_course_item($courseid);
1142     if ($progress == null) {
1143         $progress = new \core\progress\none();
1144     }
1146     if ($userid) {
1147         // one raw grade updated for one user
1148         if (empty($updated_item)) {
1149             print_error("cannotbenull", 'debug', '', "updated_item");
1150         }
1151         if ($course_item->needsupdate) {
1152             $updated_item->force_regrading();
1153             return array($course_item->id =>'Can not do fast regrading after updating of raw grades');
1154         }
1156     } else {
1157         if (!$course_item->needsupdate) {
1158             // nothing to do :-)
1159             return true;
1160         }
1161     }
1163     // Categories might have to run some processing before we fetch the grade items.
1164     // This gives them a final opportunity to update and mark their children to be updated.
1165     // We need to work on the children categories up to the parent ones, so that, for instance,
1166     // if a category total is updated it will be reflected in the parent category.
1167     $cats = grade_category::fetch_all(array('courseid' => $courseid));
1168     $flatcattree = array();
1169     foreach ($cats as $cat) {
1170         if (!isset($flatcattree[$cat->depth])) {
1171             $flatcattree[$cat->depth] = array();
1172         }
1173         $flatcattree[$cat->depth][] = $cat;
1174     }
1175     krsort($flatcattree);
1176     foreach ($flatcattree as $depth => $cats) {
1177         foreach ($cats as $cat) {
1178             $cat->pre_regrade_final_grades();
1179         }
1180     }
1182     $progresstotal = 0;
1183     $progresscurrent = 0;
1185     $grade_items = grade_item::fetch_all(array('courseid'=>$courseid));
1186     $depends_on = array();
1188     foreach ($grade_items as $gid=>$gitem) {
1189         if ((!empty($updated_item) and $updated_item->id == $gid) ||
1190                 $gitem->is_course_item() || $gitem->is_category_item() || $gitem->is_calculated()) {
1191             $grade_items[$gid]->needsupdate = 1;
1192         }
1194         // We load all dependencies of these items later we can discard some grade_items based on this.
1195         if ($grade_items[$gid]->needsupdate) {
1196             $depends_on[$gid] = $grade_items[$gid]->depends_on();
1197             $progresstotal++;
1198         }
1199     }
1201     $progress->start_progress('regrade_course', $progresstotal);
1203     $errors = array();
1204     $finalids = array();
1205     $updatedids = array();
1206     $gids     = array_keys($grade_items);
1207     $failed = 0;
1209     while (count($finalids) < count($gids)) { // work until all grades are final or error found
1210         $count = 0;
1211         foreach ($gids as $gid) {
1212             if (in_array($gid, $finalids)) {
1213                 continue; // already final
1214             }
1216             if (!$grade_items[$gid]->needsupdate) {
1217                 $finalids[] = $gid; // we can make it final - does not need update
1218                 continue;
1219             }
1220             $thisprogress = $progresstotal;
1221             foreach ($grade_items as $item) {
1222                 if ($item->needsupdate) {
1223                     $thisprogress--;
1224                 }
1225             }
1226             // Clip between $progresscurrent and $progresstotal.
1227             $thisprogress = max(min($thisprogress, $progresstotal), $progresscurrent);
1228             $progress->progress($thisprogress);
1229             $progresscurrent = $thisprogress;
1231             foreach ($depends_on[$gid] as $did) {
1232                 if (!in_array($did, $finalids)) {
1233                     // This item depends on something that is not yet in finals array.
1234                     continue 2;
1235                 }
1236             }
1238             // If this grade item has no dependancy with any updated item at all, then remove it from being recalculated.
1240             // When we get here, all of this grade item's decendents are marked as final so they would be marked as updated too
1241             // if they would have been regraded. We don't need to regrade items which dependants (not only the direct ones
1242             // but any dependant in the cascade) have not been updated.
1244             // If $updated_item was specified we discard the grade items that do not depend on it or on any grade item that
1245             // depend on $updated_item.
1247             // Here we check to see if the direct decendants are marked as updated.
1248             if (!empty($updated_item) && $gid != $updated_item->id && !in_array($updated_item->id, $depends_on[$gid])) {
1250                 // We need to ensure that none of this item's dependencies have been updated.
1251                 // If we find that one of the direct decendants of this grade item is marked as updated then this
1252                 // grade item needs to be recalculated and marked as updated.
1253                 // Being marked as updated is done further down in the code.
1255                 $updateddependencies = false;
1256                 foreach ($depends_on[$gid] as $dependency) {
1257                     if (in_array($dependency, $updatedids)) {
1258                         $updateddependencies = true;
1259                         break;
1260                     }
1261                 }
1262                 if ($updateddependencies === false) {
1263                     // If no direct descendants are marked as updated, then we don't need to update this grade item. We then mark it
1264                     // as final.
1265                     $count++;
1266                     $finalids[] = $gid;
1267                     continue;
1268                 }
1269             }
1271             // Let's update, calculate or aggregate.
1272             $result = $grade_items[$gid]->regrade_final_grades($userid);
1274             if ($result === true) {
1276                 // We should only update the database if we regraded all users.
1277                 if (empty($userid)) {
1278                     $grade_items[$gid]->regrading_finished();
1279                     // Do the locktime item locking.
1280                     $grade_items[$gid]->check_locktime();
1281                 } else {
1282                     $grade_items[$gid]->needsupdate = 0;
1283                 }
1284                 $count++;
1285                 $finalids[] = $gid;
1286                 $updatedids[] = $gid;
1288             } else {
1289                 $grade_items[$gid]->force_regrading();
1290                 $errors[$gid] = $result;
1291             }
1292         }
1294         if ($count == 0) {
1295             $failed++;
1296         } else {
1297             $failed = 0;
1298         }
1300         if ($failed > 1) {
1301             foreach($gids as $gid) {
1302                 if (in_array($gid, $finalids)) {
1303                     continue; // this one is ok
1304                 }
1305                 $grade_items[$gid]->force_regrading();
1306                 $errors[$grade_items[$gid]->id] = get_string('errorcalculationbroken', 'grades');
1307             }
1308             break; // Found error.
1309         }
1310     }
1311     $progress->end_progress();
1313     if (count($errors) == 0) {
1314         if (empty($userid)) {
1315             // do the locktime locking of grades, but only when doing full regrading
1316             grade_grade::check_locktime_all($gids);
1317         }
1318         return true;
1319     } else {
1320         return $errors;
1321     }
1324 /**
1325  * Refetches grade data from course activities
1326  *
1327  * @param int $courseid The course ID
1328  * @param string $modname Limit the grade fetch to a single module type. For example 'forum'
1329  * @param int $userid limit the grade fetch to a single user
1330  */
1331 function grade_grab_course_grades($courseid, $modname=null, $userid=0) {
1332     global $CFG, $DB;
1334     if ($modname) {
1335         $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname
1336                   FROM {".$modname."} a, {course_modules} cm, {modules} m
1337                  WHERE m.name=:modname AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=:courseid";
1338         $params = array('modname'=>$modname, 'courseid'=>$courseid);
1340         if ($modinstances = $DB->get_records_sql($sql, $params)) {
1341             foreach ($modinstances as $modinstance) {
1342                 grade_update_mod_grades($modinstance, $userid);
1343             }
1344         }
1345         return;
1346     }
1348     if (!$mods = core_component::get_plugin_list('mod') ) {
1349         print_error('nomodules', 'debug');
1350     }
1352     foreach ($mods as $mod => $fullmod) {
1353         if ($mod == 'NEWMODULE') {   // Someone has unzipped the template, ignore it
1354             continue;
1355         }
1357         // include the module lib once
1358         if (file_exists($fullmod.'/lib.php')) {
1359             // get all instance of the activity
1360             $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname
1361                       FROM {".$mod."} a, {course_modules} cm, {modules} m
1362                      WHERE m.name=:mod AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=:courseid";
1363             $params = array('mod'=>$mod, 'courseid'=>$courseid);
1365             if ($modinstances = $DB->get_records_sql($sql, $params)) {
1366                 foreach ($modinstances as $modinstance) {
1367                     grade_update_mod_grades($modinstance, $userid);
1368                 }
1369             }
1370         }
1371     }
1374 /**
1375  * Force full update of module grades in central gradebook
1376  *
1377  * @param object $modinstance Module object with extra cmidnumber and modname property
1378  * @param int $userid Optional user ID if limiting the update to a single user
1379  * @return bool True if success
1380  */
1381 function grade_update_mod_grades($modinstance, $userid=0) {
1382     global $CFG, $DB;
1384     $fullmod = $CFG->dirroot.'/mod/'.$modinstance->modname;
1385     if (!file_exists($fullmod.'/lib.php')) {
1386         debugging('missing lib.php file in module ' . $modinstance->modname);
1387         return false;
1388     }
1389     include_once($fullmod.'/lib.php');
1391     $updateitemfunc   = $modinstance->modname.'_grade_item_update';
1392     $updategradesfunc = $modinstance->modname.'_update_grades';
1394     if (function_exists($updategradesfunc) and function_exists($updateitemfunc)) {
1395         //new grading supported, force updating of grades
1396         $updateitemfunc($modinstance);
1397         $updategradesfunc($modinstance, $userid);
1398     } else if (function_exists($updategradesfunc) xor function_exists($updateitemfunc)) {
1399         // Module does not support grading?
1400         debugging("You have declared one of $updateitemfunc and $updategradesfunc but not both. " .
1401                   "This will cause broken behaviour.", DEBUG_DEVELOPER);
1402     }
1404     return true;
1407 /**
1408  * Remove grade letters for given context
1409  *
1410  * @param context $context The context
1411  * @param bool $showfeedback If true a success notification will be displayed
1412  */
1413 function remove_grade_letters($context, $showfeedback) {
1414     global $DB, $OUTPUT;
1416     $strdeleted = get_string('deleted');
1418     $records = $DB->get_records('grade_letters', array('contextid' => $context->id));
1419     foreach ($records as $record) {
1420         $DB->delete_records('grade_letters', array('id' => $record->id));
1421         // Trigger the letter grade deleted event.
1422         $event = \core\event\grade_letter_deleted::create(array(
1423             'objectid' => $record->id,
1424             'context' => $context,
1425         ));
1426         $event->trigger();
1427     }
1428     if ($showfeedback) {
1429         echo $OUTPUT->notification($strdeleted.' - '.get_string('letters', 'grades'), 'notifysuccess');
1430     }
1433 /**
1434  * Remove all grade related course data
1435  * Grade history is kept
1436  *
1437  * @param int $courseid The course ID
1438  * @param bool $showfeedback If true success notifications will be displayed
1439  */
1440 function remove_course_grades($courseid, $showfeedback) {
1441     global $DB, $OUTPUT;
1443     $fs = get_file_storage();
1444     $strdeleted = get_string('deleted');
1446     $course_category = grade_category::fetch_course_category($courseid);
1447     $course_category->delete('coursedelete');
1448     $fs->delete_area_files(context_course::instance($courseid)->id, 'grade', 'feedback');
1449     if ($showfeedback) {
1450         echo $OUTPUT->notification($strdeleted.' - '.get_string('grades', 'grades').', '.get_string('items', 'grades').', '.get_string('categories', 'grades'), 'notifysuccess');
1451     }
1453     if ($outcomes = grade_outcome::fetch_all(array('courseid'=>$courseid))) {
1454         foreach ($outcomes as $outcome) {
1455             $outcome->delete('coursedelete');
1456         }
1457     }
1458     $DB->delete_records('grade_outcomes_courses', array('courseid'=>$courseid));
1459     if ($showfeedback) {
1460         echo $OUTPUT->notification($strdeleted.' - '.get_string('outcomes', 'grades'), 'notifysuccess');
1461     }
1463     if ($scales = grade_scale::fetch_all(array('courseid'=>$courseid))) {
1464         foreach ($scales as $scale) {
1465             $scale->delete('coursedelete');
1466         }
1467     }
1468     if ($showfeedback) {
1469         echo $OUTPUT->notification($strdeleted.' - '.get_string('scales'), 'notifysuccess');
1470     }
1472     $DB->delete_records('grade_settings', array('courseid'=>$courseid));
1473     if ($showfeedback) {
1474         echo $OUTPUT->notification($strdeleted.' - '.get_string('settings', 'grades'), 'notifysuccess');
1475     }
1478 /**
1479  * Called when course category is deleted
1480  * Cleans the gradebook of associated data
1481  *
1482  * @param int $categoryid The course category id
1483  * @param int $newparentid If empty everything is deleted. Otherwise the ID of the category where content moved
1484  * @param bool $showfeedback print feedback
1485  */
1486 function grade_course_category_delete($categoryid, $newparentid, $showfeedback) {
1487     global $DB;
1489     $context = context_coursecat::instance($categoryid);
1490     $records = $DB->get_records('grade_letters', array('contextid' => $context->id));
1491     foreach ($records as $record) {
1492         $DB->delete_records('grade_letters', array('id' => $record->id));
1493         // Trigger the letter grade deleted event.
1494         $event = \core\event\grade_letter_deleted::create(array(
1495             'objectid' => $record->id,
1496             'context' => $context,
1497         ));
1498         $event->trigger();
1499     }
1502 /**
1503  * Does gradebook cleanup when a module is uninstalled
1504  * Deletes all associated grade items
1505  *
1506  * @param string $modname The grade item module name to remove. For example 'forum'
1507  */
1508 function grade_uninstalled_module($modname) {
1509     global $CFG, $DB;
1511     $sql = "SELECT *
1512               FROM {grade_items}
1513              WHERE itemtype='mod' AND itemmodule=?";
1515     // go all items for this module and delete them including the grades
1516     $rs = $DB->get_recordset_sql($sql, array($modname));
1517     foreach ($rs as $item) {
1518         $grade_item = new grade_item($item, false);
1519         $grade_item->delete('moduninstall');
1520     }
1521     $rs->close();
1524 /**
1525  * Deletes all of a user's grade data from gradebook
1526  *
1527  * @param int $userid The user whose grade data should be deleted
1528  */
1529 function grade_user_delete($userid) {
1530     if ($grades = grade_grade::fetch_all(array('userid'=>$userid))) {
1531         foreach ($grades as $grade) {
1532             $grade->delete('userdelete');
1533         }
1534     }
1537 /**
1538  * Purge course data when user unenrolls from a course
1539  *
1540  * @param int $courseid The ID of the course the user has unenrolled from
1541  * @param int $userid The ID of the user unenrolling
1542  */
1543 function grade_user_unenrol($courseid, $userid) {
1544     if ($items = grade_item::fetch_all(array('courseid'=>$courseid))) {
1545         foreach ($items as $item) {
1546             if ($grades = grade_grade::fetch_all(array('userid'=>$userid, 'itemid'=>$item->id))) {
1547                 foreach ($grades as $grade) {
1548                     $grade->delete('userdelete');
1549                 }
1550             }
1551         }
1552     }
1555 /**
1556  * Grading cron job. Performs background clean up on the gradebook
1557  */
1558 function grade_cron() {
1559     global $CFG, $DB;
1561     $now = time();
1563     $sql = "SELECT i.*
1564               FROM {grade_items} i
1565              WHERE i.locked = 0 AND i.locktime > 0 AND i.locktime < ? AND EXISTS (
1566                 SELECT 'x' FROM {grade_items} c WHERE c.itemtype='course' AND c.needsupdate=0 AND c.courseid=i.courseid)";
1568     // go through all courses that have proper final grades and lock them if needed
1569     $rs = $DB->get_recordset_sql($sql, array($now));
1570     foreach ($rs as $item) {
1571         $grade_item = new grade_item($item, false);
1572         $grade_item->locked = $now;
1573         $grade_item->update('locktime');
1574     }
1575     $rs->close();
1577     $grade_inst = new grade_grade();
1578     $fields = 'g.'.implode(',g.', $grade_inst->required_fields);
1580     $sql = "SELECT $fields
1581               FROM {grade_grades} g, {grade_items} i
1582              WHERE g.locked = 0 AND g.locktime > 0 AND g.locktime < ? AND g.itemid=i.id AND EXISTS (
1583                 SELECT 'x' FROM {grade_items} c WHERE c.itemtype='course' AND c.needsupdate=0 AND c.courseid=i.courseid)";
1585     // go through all courses that have proper final grades and lock them if needed
1586     $rs = $DB->get_recordset_sql($sql, array($now));
1587     foreach ($rs as $grade) {
1588         $grade_grade = new grade_grade($grade, false);
1589         $grade_grade->locked = $now;
1590         $grade_grade->update('locktime');
1591     }
1592     $rs->close();
1594     //TODO: do not run this cleanup every cron invocation
1595     // cleanup history tables
1596     if (!empty($CFG->gradehistorylifetime)) {  // value in days
1597         $histlifetime = $now - ($CFG->gradehistorylifetime * 3600 * 24);
1598         $tables = array('grade_outcomes_history', 'grade_categories_history', 'grade_items_history', 'grade_grades_history', 'scale_history');
1599         foreach ($tables as $table) {
1600             if ($DB->delete_records_select($table, "timemodified < ?", array($histlifetime))) {
1601                 mtrace("    Deleted old grade history records from '$table'");
1602             }
1603         }
1604     }
1607 /**
1608  * Reset all course grades, refetch from the activities and recalculate
1609  *
1610  * @param int $courseid The course to reset
1611  * @return bool success
1612  */
1613 function grade_course_reset($courseid) {
1615     // no recalculations
1616     grade_force_full_regrading($courseid);
1618     $grade_items = grade_item::fetch_all(array('courseid'=>$courseid));
1619     foreach ($grade_items as $gid=>$grade_item) {
1620         $grade_item->delete_all_grades('reset');
1621     }
1623     //refetch all grades
1624     grade_grab_course_grades($courseid);
1626     // recalculate all grades
1627     grade_regrade_final_grades($courseid);
1628     return true;
1631 /**
1632  * Convert a number to 5 decimal point float, an empty string or a null db compatible format
1633  * (we need this to decide if db value changed)
1634  *
1635  * @param mixed $number The number to convert
1636  * @return mixed float or null
1637  */
1638 function grade_floatval($number) {
1639     if (is_null($number) or $number === '') {
1640         return null;
1641     }
1642     // we must round to 5 digits to get the same precision as in 10,5 db fields
1643     // note: db rounding for 10,5 is different from php round() function
1644     return round($number, 5);
1647 /**
1648  * Compare two float numbers safely. Uses 5 decimals php precision using {@link grade_floatval()}. Nulls accepted too.
1649  * Used for determining if a database update is required
1650  *
1651  * @param float $f1 Float one to compare
1652  * @param float $f2 Float two to compare
1653  * @return bool True if the supplied values are different
1654  */
1655 function grade_floats_different($f1, $f2) {
1656     // note: db rounding for 10,5 is different from php round() function
1657     return (grade_floatval($f1) !== grade_floatval($f2));
1660 /**
1661  * Compare two float numbers safely. Uses 5 decimals php precision using {@link grade_floatval()}
1662  *
1663  * Do not use rounding for 10,5 at the database level as the results may be
1664  * different from php round() function.
1665  *
1666  * @since Moodle 2.0
1667  * @param float $f1 Float one to compare
1668  * @param float $f2 Float two to compare
1669  * @return bool True if the values should be considered as the same grades
1670  */
1671 function grade_floats_equal($f1, $f2) {
1672     return (grade_floatval($f1) === grade_floatval($f2));