MDL-51360 core_grades: Improve documentation of grade_get_grades().
[moodle.git] / lib / gradelib.php
bloba0e444744d1d9706b1000eca4d14ec8356076f86
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17 /**
18 * Library of functions for gradebook - both public and internal
20 * @package core_grades
21 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 defined('MOODLE_INTERNAL') || die();
27 global $CFG;
29 /** Include essential files */
30 require_once($CFG->libdir . '/grade/constants.php');
32 require_once($CFG->libdir . '/grade/grade_category.php');
33 require_once($CFG->libdir . '/grade/grade_item.php');
34 require_once($CFG->libdir . '/grade/grade_grade.php');
35 require_once($CFG->libdir . '/grade/grade_scale.php');
36 require_once($CFG->libdir . '/grade/grade_outcome.php');
38 /////////////////////////////////////////////////////////////////////
39 ///// Start of public API for communication with modules/blocks /////
40 /////////////////////////////////////////////////////////////////////
42 /**
43 * Submit new or update grade; update/create grade_item definition. Grade must have userid specified,
44 * rawgrade and feedback with format are optional. rawgrade NULL means 'Not graded'.
45 * Missing property or key means does not change the existing value.
47 * Only following grade item properties can be changed 'itemname', 'idnumber', 'gradetype', 'grademax',
48 * 'grademin', 'scaleid', 'multfactor', 'plusfactor', 'deleted' and 'hidden'. 'reset' means delete all current grades including locked ones.
50 * Manual, course or category items can not be updated by this function.
52 * @category grade
53 * @param string $source Source of the grade such as 'mod/assignment'
54 * @param int $courseid ID of course
55 * @param string $itemtype Type of grade item. For example, mod or block
56 * @param string $itemmodule More specific then $itemtype. For example, assignment or forum. May be NULL for some item types
57 * @param int $iteminstance Instance ID of graded item
58 * @param int $itemnumber Most probably 0. Modules can use other numbers when having more than one grade for each user
59 * @param mixed $grades Grade (object, array) or several grades (arrays of arrays or objects), NULL if updating grade_item definition only
60 * @param mixed $itemdetails Object or array describing the grading item, NULL if no change
61 * @param bool $isbulkupdate If bulk grade update is happening.
62 * @return int Returns GRADE_UPDATE_OK, GRADE_UPDATE_FAILED, GRADE_UPDATE_MULTIPLE or GRADE_UPDATE_ITEM_LOCKED
64 function grade_update($source, $courseid, $itemtype, $itemmodule, $iteminstance, $itemnumber, $grades = null,
65 $itemdetails = null, $isbulkupdate = false) {
66 global $USER, $CFG, $DB;
68 // only following grade_item properties can be changed in this function
69 $allowed = array('itemname', 'idnumber', 'gradetype', 'grademax', 'grademin', 'scaleid', 'multfactor', 'plusfactor', 'deleted', 'hidden');
70 // list of 10,5 numeric fields
71 $floats = array('grademin', 'grademax', 'multfactor', 'plusfactor');
73 // grade item identification
74 $params = compact('courseid', 'itemtype', 'itemmodule', 'iteminstance', 'itemnumber');
76 if (is_null($courseid) or is_null($itemtype)) {
77 debugging('Missing courseid or itemtype');
78 return GRADE_UPDATE_FAILED;
81 if (!$gradeitems = grade_item::fetch_all($params)) {
82 // create a new one
83 $gradeitem = false;
84 } else if (count($gradeitems) == 1) {
85 $gradeitem = reset($gradeitems);
86 unset($gradeitems); // Release memory.
87 } else {
88 debugging('Found more than one grade item');
89 return GRADE_UPDATE_MULTIPLE;
92 if (!empty($itemdetails['deleted'])) {
93 if ($gradeitem) {
94 if ($gradeitem->delete($source)) {
95 return GRADE_UPDATE_OK;
96 } else {
97 return GRADE_UPDATE_FAILED;
100 return GRADE_UPDATE_OK;
103 /// Create or update the grade_item if needed
105 if (!$gradeitem) {
106 if ($itemdetails) {
107 $itemdetails = (array)$itemdetails;
109 // grademin and grademax ignored when scale specified
110 if (array_key_exists('scaleid', $itemdetails)) {
111 if ($itemdetails['scaleid']) {
112 unset($itemdetails['grademin']);
113 unset($itemdetails['grademax']);
117 foreach ($itemdetails as $k=>$v) {
118 if (!in_array($k, $allowed)) {
119 // ignore it
120 continue;
122 if ($k == 'gradetype' and $v == GRADE_TYPE_NONE) {
123 // no grade item needed!
124 return GRADE_UPDATE_OK;
126 $params[$k] = $v;
129 $gradeitem = new grade_item($params);
130 $gradeitem->insert(null, $isbulkupdate);
132 } else {
133 if ($gradeitem->is_locked()) {
134 // no notice() here, test returned value instead!
135 return GRADE_UPDATE_ITEM_LOCKED;
138 if ($itemdetails) {
139 $itemdetails = (array)$itemdetails;
140 $update = false;
141 foreach ($itemdetails as $k=>$v) {
142 if (!in_array($k, $allowed)) {
143 // ignore it
144 continue;
146 if (in_array($k, $floats)) {
147 if (grade_floats_different($gradeitem->{$k}, $v)) {
148 $gradeitem->{$k} = $v;
149 $update = true;
152 } else {
153 if ($gradeitem->{$k} != $v) {
154 $gradeitem->{$k} = $v;
155 $update = true;
159 if ($update) {
160 $gradeitem->update(null, $isbulkupdate);
165 /// reset grades if requested
166 if (!empty($itemdetails['reset'])) {
167 $gradeitem->delete_all_grades('reset');
168 return GRADE_UPDATE_OK;
171 /// Some extra checks
172 // do we use grading?
173 if ($gradeitem->gradetype == GRADE_TYPE_NONE) {
174 return GRADE_UPDATE_OK;
177 // no grade submitted
178 if (empty($grades)) {
179 return GRADE_UPDATE_OK;
182 /// Finally start processing of grades
183 if (is_object($grades)) {
184 $grades = array($grades->userid=>$grades);
185 } else {
186 if (array_key_exists('userid', $grades)) {
187 $grades = array($grades['userid']=>$grades);
191 /// normalize and verify grade array
192 foreach($grades as $k=>$g) {
193 if (!is_array($g)) {
194 $g = (array)$g;
195 $grades[$k] = $g;
198 if (empty($g['userid']) or $k != $g['userid']) {
199 debugging('Incorrect grade array index, must be user id! Grade ignored.');
200 unset($grades[$k]);
204 if (empty($grades)) {
205 return GRADE_UPDATE_FAILED;
208 $count = count($grades);
209 if ($count > 0 and $count < 200) {
210 list($uids, $params) = $DB->get_in_or_equal(array_keys($grades), SQL_PARAMS_NAMED, $start='uid');
211 $params['gid'] = $gradeitem->id;
212 $sql = "SELECT * FROM {grade_grades} WHERE itemid = :gid AND userid $uids";
214 } else {
215 $sql = "SELECT * FROM {grade_grades} WHERE itemid = :gid";
216 $params = array('gid' => $gradeitem->id);
219 $rs = $DB->get_recordset_sql($sql, $params);
221 $failed = false;
223 while (count($grades) > 0) {
224 $gradegrade = null;
225 $grade = null;
227 foreach ($rs as $gd) {
229 $userid = $gd->userid;
230 if (!isset($grades[$userid])) {
231 // this grade not requested, continue
232 continue;
234 // existing grade requested
235 $grade = $grades[$userid];
236 $gradegrade = new grade_grade($gd, false);
237 unset($grades[$userid]);
238 break;
241 if (is_null($gradegrade)) {
242 if (count($grades) == 0) {
243 // No more grades to process.
244 break;
247 $grade = reset($grades);
248 $userid = $grade['userid'];
249 $gradegrade = new grade_grade(array('itemid' => $gradeitem->id, 'userid' => $userid), false);
250 $gradegrade->load_optional_fields(); // add feedback and info too
251 unset($grades[$userid]);
254 $rawgrade = false;
255 $feedback = false;
256 $feedbackformat = FORMAT_MOODLE;
257 $feedbackfiles = [];
258 $usermodified = $USER->id;
259 $datesubmitted = null;
260 $dategraded = null;
262 if (array_key_exists('rawgrade', $grade)) {
263 $rawgrade = $grade['rawgrade'];
266 if (array_key_exists('feedback', $grade)) {
267 $feedback = $grade['feedback'];
270 if (array_key_exists('feedbackformat', $grade)) {
271 $feedbackformat = $grade['feedbackformat'];
274 if (array_key_exists('feedbackfiles', $grade)) {
275 $feedbackfiles = $grade['feedbackfiles'];
278 if (array_key_exists('usermodified', $grade)) {
279 $usermodified = $grade['usermodified'];
282 if (array_key_exists('datesubmitted', $grade)) {
283 $datesubmitted = $grade['datesubmitted'];
286 if (array_key_exists('dategraded', $grade)) {
287 $dategraded = $grade['dategraded'];
290 // update or insert the grade
291 if (!$gradeitem->update_raw_grade($userid, $rawgrade, $source, $feedback, $feedbackformat, $usermodified,
292 $dategraded, $datesubmitted, $gradegrade, $feedbackfiles, $isbulkupdate)) {
293 $failed = true;
297 if ($rs) {
298 $rs->close();
301 if (!$failed) {
302 return GRADE_UPDATE_OK;
303 } else {
304 return GRADE_UPDATE_FAILED;
309 * Updates a user's outcomes. Manual outcomes can not be updated.
311 * @category grade
312 * @param string $source Source of the grade such as 'mod/assignment'
313 * @param int $courseid ID of course
314 * @param string $itemtype Type of grade item. For example, 'mod' or 'block'
315 * @param string $itemmodule More specific then $itemtype. For example, 'forum' or 'quiz'. May be NULL for some item types
316 * @param int $iteminstance Instance ID of graded item. For example the forum ID.
317 * @param int $userid ID of the graded user
318 * @param array $data Array consisting of grade item itemnumber ({@link grade_update()}) => outcomegrade
319 * @return bool returns true if grade items were found and updated successfully
321 function grade_update_outcomes($source, $courseid, $itemtype, $itemmodule, $iteminstance, $userid, $data) {
322 if ($items = grade_item::fetch_all(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid))) {
323 $result = true;
324 foreach ($items as $item) {
325 if (!array_key_exists($item->itemnumber, $data)) {
326 continue;
328 $grade = $data[$item->itemnumber] < 1 ? null : $data[$item->itemnumber];
329 $result = ($item->update_final_grade($userid, $grade, $source) && $result);
331 return $result;
333 return false; //grade items not found
337 * Return true if the course needs regrading.
339 * @param int $courseid The course ID
340 * @return bool true if course grades need updating.
342 function grade_needs_regrade_final_grades($courseid) {
343 $course_item = grade_item::fetch_course_item($courseid);
344 return $course_item->needsupdate;
348 * Return true if the regrade process is likely to be time consuming and
349 * will therefore require the progress bar.
351 * @param int $courseid The course ID
352 * @return bool Whether the regrade process is likely to be time consuming
354 function grade_needs_regrade_progress_bar($courseid) {
355 global $DB;
356 $grade_items = grade_item::fetch_all(array('courseid' => $courseid));
357 if (!$grade_items) {
358 // If there are no grade items then we definitely don't need a progress bar!
359 return false;
362 list($sql, $params) = $DB->get_in_or_equal(array_keys($grade_items), SQL_PARAMS_NAMED, 'gi');
363 $gradecount = $DB->count_records_select('grade_grades', 'itemid ' . $sql, $params);
365 // This figure may seem arbitrary, but after analysis it seems that 100 grade_grades can be calculated in ~= 0.5 seconds.
366 // Any longer than this and we want to show the progress bar.
367 return $gradecount > 100;
371 * Check whether regarding of final grades is required and, if so, perform the regrade.
373 * If the regrade is expected to be time consuming (see grade_needs_regrade_progress_bar), then this
374 * function will output the progress bar, and redirect to the current PAGE->url after regrading
375 * completes. Otherwise the regrading will happen immediately and the page will be loaded as per
376 * normal.
378 * A callback may be specified, which is called if regrading has taken place.
379 * The callback may optionally return a URL which will be redirected to when the progress bar is present.
381 * @param stdClass $course The course to regrade
382 * @param callable $callback A function to call if regrading took place
383 * @return moodle_url|false The URL to redirect to if redirecting
385 function grade_regrade_final_grades_if_required($course, callable $callback = null) {
386 global $PAGE, $OUTPUT;
388 if (!grade_needs_regrade_final_grades($course->id)) {
389 return false;
392 if (grade_needs_regrade_progress_bar($course->id)) {
393 if ($PAGE->state !== moodle_page::STATE_IN_BODY) {
394 $PAGE->set_heading($course->fullname);
395 echo $OUTPUT->header();
397 echo $OUTPUT->heading(get_string('recalculatinggrades', 'grades'));
398 $progress = new \core\progress\display(true);
399 $status = grade_regrade_final_grades($course->id, null, null, $progress);
401 // Show regrade errors and set the course to no longer needing regrade (stop endless loop).
402 if (is_array($status)) {
403 foreach ($status as $error) {
404 $errortext = new \core\output\notification($error, \core\output\notification::NOTIFY_ERROR);
405 echo $OUTPUT->render($errortext);
407 $courseitem = grade_item::fetch_course_item($course->id);
408 $courseitem->regrading_finished();
411 if ($callback) {
413 $url = call_user_func($callback);
416 if (empty($url)) {
417 $url = $PAGE->url;
420 echo $OUTPUT->continue_button($url);
421 echo $OUTPUT->footer();
422 die();
423 } else {
424 $result = grade_regrade_final_grades($course->id);
425 if ($callback) {
426 call_user_func($callback);
428 return $result;
433 * Returns grading information for given activity, optionally with user grades.
434 * Manual, course or category items can not be queried.
436 * This function can be VERY costly - it is doing full course grades recalculation if needsupdate = 1
437 * for course grade item. So be sure you really need it.
438 * If you need just certain grades consider using grade_item::refresh_grades()
439 * together with grade_item::get_grade() instead.
441 * @param int $courseid ID of course
442 * @param string $itemtype Type of grade item. For example, 'mod' or 'block'
443 * @param string $itemmodule More specific then $itemtype. For example, 'forum' or 'quiz'. May be NULL for some item types
444 * @param int $iteminstance ID of the item module
445 * @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
446 * @return stdClass Object with keys {items, outcomes, errors}, where 'items' is an array of grade
447 * information objects (scaleid, name, grade and locked status, etc.) indexed with itemnumbers
448 * @category grade
450 function grade_get_grades($courseid, $itemtype, $itemmodule, $iteminstance, $userid_or_ids=null) {
451 global $CFG;
453 $return = new stdClass();
454 $return->items = array();
455 $return->outcomes = array();
456 $return->errors = [];
458 $courseitem = grade_item::fetch_course_item($courseid);
459 $needsupdate = array();
460 if ($courseitem->needsupdate) {
461 $result = grade_regrade_final_grades($courseid);
462 if ($result !== true) {
463 $needsupdate = array_keys($result);
464 // Return regrade errors if the user has capability.
465 $context = context_course::instance($courseid);
466 if (has_capability('moodle/grade:edit', $context)) {
467 $return->errors = $result;
469 $courseitem->regrading_finished();
473 if ($grade_items = grade_item::fetch_all(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid))) {
474 foreach ($grade_items as $grade_item) {
475 $decimalpoints = null;
477 if (empty($grade_item->outcomeid)) {
478 // prepare information about grade item
479 $item = new stdClass();
480 $item->id = $grade_item->id;
481 $item->itemnumber = $grade_item->itemnumber;
482 $item->itemtype = $grade_item->itemtype;
483 $item->itemmodule = $grade_item->itemmodule;
484 $item->iteminstance = $grade_item->iteminstance;
485 $item->scaleid = $grade_item->scaleid;
486 $item->name = $grade_item->get_name();
487 $item->grademin = $grade_item->grademin;
488 $item->grademax = $grade_item->grademax;
489 $item->gradepass = $grade_item->gradepass;
490 $item->locked = $grade_item->is_locked();
491 $item->hidden = $grade_item->is_hidden();
492 $item->grades = array();
494 switch ($grade_item->gradetype) {
495 case GRADE_TYPE_NONE:
496 break;
498 case GRADE_TYPE_VALUE:
499 $item->scaleid = 0;
500 break;
502 case GRADE_TYPE_TEXT:
503 $item->scaleid = 0;
504 $item->grademin = 0;
505 $item->grademax = 0;
506 $item->gradepass = 0;
507 break;
510 if (empty($userid_or_ids)) {
511 $userids = array();
513 } else if (is_array($userid_or_ids)) {
514 $userids = $userid_or_ids;
516 } else {
517 $userids = array($userid_or_ids);
520 if ($userids) {
521 $grade_grades = grade_grade::fetch_users_grades($grade_item, $userids, true);
522 foreach ($userids as $userid) {
523 $grade_grades[$userid]->grade_item =& $grade_item;
525 $grade = new stdClass();
526 $grade->grade = $grade_grades[$userid]->finalgrade;
527 $grade->locked = $grade_grades[$userid]->is_locked();
528 $grade->hidden = $grade_grades[$userid]->is_hidden();
529 $grade->overridden = $grade_grades[$userid]->overridden;
530 $grade->feedback = $grade_grades[$userid]->feedback;
531 $grade->feedbackformat = $grade_grades[$userid]->feedbackformat;
532 $grade->usermodified = $grade_grades[$userid]->usermodified;
533 $grade->datesubmitted = $grade_grades[$userid]->get_datesubmitted();
534 $grade->dategraded = $grade_grades[$userid]->get_dategraded();
536 // create text representation of grade
537 if ($grade_item->gradetype == GRADE_TYPE_TEXT or $grade_item->gradetype == GRADE_TYPE_NONE) {
538 $grade->grade = null;
539 $grade->str_grade = '-';
540 $grade->str_long_grade = $grade->str_grade;
542 } else if (in_array($grade_item->id, $needsupdate)) {
543 $grade->grade = false;
544 $grade->str_grade = get_string('error');
545 $grade->str_long_grade = $grade->str_grade;
547 } else if (is_null($grade->grade)) {
548 $grade->str_grade = '-';
549 $grade->str_long_grade = $grade->str_grade;
551 } else {
552 $grade->str_grade = grade_format_gradevalue($grade->grade, $grade_item);
553 if ($grade_item->gradetype == GRADE_TYPE_SCALE or $grade_item->get_displaytype() != GRADE_DISPLAY_TYPE_REAL) {
554 $grade->str_long_grade = $grade->str_grade;
555 } else {
556 $a = new stdClass();
557 $a->grade = $grade->str_grade;
558 $a->max = grade_format_gradevalue($grade_item->grademax, $grade_item);
559 $grade->str_long_grade = get_string('gradelong', 'grades', $a);
563 // create html representation of feedback
564 if (is_null($grade->feedback)) {
565 $grade->str_feedback = '';
566 } else {
567 $feedback = file_rewrite_pluginfile_urls(
568 $grade->feedback,
569 'pluginfile.php',
570 $grade_grades[$userid]->get_context()->id,
571 GRADE_FILE_COMPONENT,
572 GRADE_FEEDBACK_FILEAREA,
573 $grade_grades[$userid]->id
576 $grade->str_feedback = format_text($feedback, $grade->feedbackformat,
577 ['context' => $grade_grades[$userid]->get_context()]);
580 $item->grades[$userid] = $grade;
583 $return->items[$grade_item->itemnumber] = $item;
585 } else {
586 if (!$grade_outcome = grade_outcome::fetch(array('id'=>$grade_item->outcomeid))) {
587 debugging('Incorect outcomeid found');
588 continue;
591 // outcome info
592 $outcome = new stdClass();
593 $outcome->id = $grade_item->id;
594 $outcome->itemnumber = $grade_item->itemnumber;
595 $outcome->itemtype = $grade_item->itemtype;
596 $outcome->itemmodule = $grade_item->itemmodule;
597 $outcome->iteminstance = $grade_item->iteminstance;
598 $outcome->scaleid = $grade_outcome->scaleid;
599 $outcome->name = $grade_outcome->get_name();
600 $outcome->locked = $grade_item->is_locked();
601 $outcome->hidden = $grade_item->is_hidden();
603 if (empty($userid_or_ids)) {
604 $userids = array();
605 } else if (is_array($userid_or_ids)) {
606 $userids = $userid_or_ids;
607 } else {
608 $userids = array($userid_or_ids);
611 if ($userids) {
612 $grade_grades = grade_grade::fetch_users_grades($grade_item, $userids, true);
613 foreach ($userids as $userid) {
614 $grade_grades[$userid]->grade_item =& $grade_item;
616 $grade = new stdClass();
617 $grade->grade = $grade_grades[$userid]->finalgrade;
618 $grade->locked = $grade_grades[$userid]->is_locked();
619 $grade->hidden = $grade_grades[$userid]->is_hidden();
620 $grade->feedback = $grade_grades[$userid]->feedback;
621 $grade->feedbackformat = $grade_grades[$userid]->feedbackformat;
622 $grade->usermodified = $grade_grades[$userid]->usermodified;
623 $grade->datesubmitted = $grade_grades[$userid]->get_datesubmitted();
624 $grade->dategraded = $grade_grades[$userid]->get_dategraded();
626 // create text representation of grade
627 if (in_array($grade_item->id, $needsupdate)) {
628 $grade->grade = false;
629 $grade->str_grade = get_string('error');
631 } else if (is_null($grade->grade)) {
632 $grade->grade = 0;
633 $grade->str_grade = get_string('nooutcome', 'grades');
635 } else {
636 $grade->grade = (int)$grade->grade;
637 $scale = $grade_item->load_scale();
638 $grade->str_grade = format_string($scale->scale_items[(int)$grade->grade-1]);
641 // create html representation of feedback
642 if (is_null($grade->feedback)) {
643 $grade->str_feedback = '';
644 } else {
645 $grade->str_feedback = format_text($grade->feedback, $grade->feedbackformat);
648 $outcome->grades[$userid] = $grade;
652 if (isset($return->outcomes[$grade_item->itemnumber])) {
653 // itemnumber duplicates - lets fix them!
654 $newnumber = $grade_item->itemnumber + 1;
655 while(grade_item::fetch(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid, 'itemnumber'=>$newnumber))) {
656 $newnumber++;
658 $outcome->itemnumber = $newnumber;
659 $grade_item->itemnumber = $newnumber;
660 $grade_item->update('system');
663 $return->outcomes[$grade_item->itemnumber] = $outcome;
669 // sort results using itemnumbers
670 ksort($return->items, SORT_NUMERIC);
671 ksort($return->outcomes, SORT_NUMERIC);
673 return $return;
676 ///////////////////////////////////////////////////////////////////
677 ///// End of public API for communication with modules/blocks /////
678 ///////////////////////////////////////////////////////////////////
682 ///////////////////////////////////////////////////////////////////
683 ///// Internal API: used by gradebook plugins and Moodle core /////
684 ///////////////////////////////////////////////////////////////////
687 * Returns a course gradebook setting
689 * @param int $courseid
690 * @param string $name of setting, maybe null if reset only
691 * @param string $default value to return if setting is not found
692 * @param bool $resetcache force reset of internal static cache
693 * @return string value of the setting, $default if setting not found, NULL if supplied $name is null
695 function grade_get_setting($courseid, $name, $default=null, $resetcache=false) {
696 global $DB;
698 $cache = cache::make('core', 'gradesetting');
699 $gradesetting = $cache->get($courseid) ?: array();
701 if ($resetcache or empty($gradesetting)) {
702 $gradesetting = array();
703 $cache->set($courseid, $gradesetting);
705 } else if (is_null($name)) {
706 return null;
708 } else if (array_key_exists($name, $gradesetting)) {
709 return $gradesetting[$name];
712 if (!$data = $DB->get_record('grade_settings', array('courseid'=>$courseid, 'name'=>$name))) {
713 $result = null;
714 } else {
715 $result = $data->value;
718 if (is_null($result)) {
719 $result = $default;
722 $gradesetting[$name] = $result;
723 $cache->set($courseid, $gradesetting);
724 return $result;
728 * Returns all course gradebook settings as object properties
730 * @param int $courseid
731 * @return object
733 function grade_get_settings($courseid) {
734 global $DB;
736 $settings = new stdClass();
737 $settings->id = $courseid;
739 if ($records = $DB->get_records('grade_settings', array('courseid'=>$courseid))) {
740 foreach ($records as $record) {
741 $settings->{$record->name} = $record->value;
745 return $settings;
749 * Add, update or delete a course gradebook setting
751 * @param int $courseid The course ID
752 * @param string $name Name of the setting
753 * @param string $value Value of the setting. NULL means delete the setting.
755 function grade_set_setting($courseid, $name, $value) {
756 global $DB;
758 if (is_null($value)) {
759 $DB->delete_records('grade_settings', array('courseid'=>$courseid, 'name'=>$name));
761 } else if (!$existing = $DB->get_record('grade_settings', array('courseid'=>$courseid, 'name'=>$name))) {
762 $data = new stdClass();
763 $data->courseid = $courseid;
764 $data->name = $name;
765 $data->value = $value;
766 $DB->insert_record('grade_settings', $data);
768 } else {
769 $data = new stdClass();
770 $data->id = $existing->id;
771 $data->value = $value;
772 $DB->update_record('grade_settings', $data);
775 grade_get_setting($courseid, null, null, true); // reset the cache
779 * Returns string representation of grade value
781 * @param float|null $value The grade value
782 * @param object $grade_item Grade item object passed by reference to prevent scale reloading
783 * @param bool $localized use localised decimal separator
784 * @param int $displaytype type of display. For example GRADE_DISPLAY_TYPE_REAL, GRADE_DISPLAY_TYPE_PERCENTAGE, GRADE_DISPLAY_TYPE_LETTER
785 * @param int $decimals The number of decimal places when displaying float values
786 * @return string
788 function grade_format_gradevalue(?float $value, &$grade_item, $localized=true, $displaytype=null, $decimals=null) {
789 if ($grade_item->gradetype == GRADE_TYPE_NONE or $grade_item->gradetype == GRADE_TYPE_TEXT) {
790 return '';
793 // no grade yet?
794 if (is_null($value)) {
795 return '-';
798 if ($grade_item->gradetype != GRADE_TYPE_VALUE and $grade_item->gradetype != GRADE_TYPE_SCALE) {
799 //unknown type??
800 return '';
803 if (is_null($displaytype)) {
804 $displaytype = $grade_item->get_displaytype();
807 if (is_null($decimals)) {
808 $decimals = $grade_item->get_decimals();
811 switch ($displaytype) {
812 case GRADE_DISPLAY_TYPE_REAL:
813 return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized);
815 case GRADE_DISPLAY_TYPE_PERCENTAGE:
816 return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized);
818 case GRADE_DISPLAY_TYPE_LETTER:
819 return grade_format_gradevalue_letter($value, $grade_item);
821 case GRADE_DISPLAY_TYPE_REAL_PERCENTAGE:
822 return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ' (' .
823 grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ')';
825 case GRADE_DISPLAY_TYPE_REAL_LETTER:
826 return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ' (' .
827 grade_format_gradevalue_letter($value, $grade_item) . ')';
829 case GRADE_DISPLAY_TYPE_PERCENTAGE_REAL:
830 return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ' (' .
831 grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ')';
833 case GRADE_DISPLAY_TYPE_LETTER_REAL:
834 return grade_format_gradevalue_letter($value, $grade_item) . ' (' .
835 grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ')';
837 case GRADE_DISPLAY_TYPE_LETTER_PERCENTAGE:
838 return grade_format_gradevalue_letter($value, $grade_item) . ' (' .
839 grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ')';
841 case GRADE_DISPLAY_TYPE_PERCENTAGE_LETTER:
842 return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ' (' .
843 grade_format_gradevalue_letter($value, $grade_item) . ')';
844 default:
845 return '';
850 * Returns a float representation of a grade value
852 * @param float|null $value The grade value
853 * @param object $grade_item Grade item object
854 * @param int $decimals The number of decimal places
855 * @param bool $localized use localised decimal separator
856 * @return string
858 function grade_format_gradevalue_real(?float $value, $grade_item, $decimals, $localized) {
859 if ($grade_item->gradetype == GRADE_TYPE_SCALE) {
860 if (!$scale = $grade_item->load_scale()) {
861 return get_string('error');
864 $value = $grade_item->bounded_grade($value);
865 return format_string($scale->scale_items[$value-1]);
867 } else {
868 return format_float($value, $decimals, $localized);
873 * Returns a percentage representation of a grade value
875 * @param float|null $value The grade value
876 * @param object $grade_item Grade item object
877 * @param int $decimals The number of decimal places
878 * @param bool $localized use localised decimal separator
879 * @return string
881 function grade_format_gradevalue_percentage(?float $value, $grade_item, $decimals, $localized) {
882 $min = $grade_item->grademin;
883 $max = $grade_item->grademax;
884 if ($min == $max) {
885 return '';
887 $value = $grade_item->bounded_grade($value);
888 $percentage = (($value-$min)*100)/($max-$min);
889 return format_float($percentage, $decimals, $localized).' %';
893 * Returns a letter grade representation of a grade value
894 * The array of grade letters used is produced by {@link grade_get_letters()} using the course context
896 * @param float|null $value The grade value
897 * @param object $grade_item Grade item object
898 * @return string
900 function grade_format_gradevalue_letter(?float $value, $grade_item) {
901 global $CFG;
902 $context = context_course::instance($grade_item->courseid, IGNORE_MISSING);
903 if (!$letters = grade_get_letters($context)) {
904 return ''; // no letters??
907 if (is_null($value)) {
908 return '-';
911 $value = grade_grade::standardise_score($value, $grade_item->grademin, $grade_item->grademax, 0, 100);
912 $value = bounded_number(0, $value, 100); // just in case
914 $gradebookcalculationsfreeze = 'gradebook_calculations_freeze_' . $grade_item->courseid;
916 foreach ($letters as $boundary => $letter) {
917 if (property_exists($CFG, $gradebookcalculationsfreeze) && (int)$CFG->{$gradebookcalculationsfreeze} <= 20160518) {
918 // Do nothing.
919 } else {
920 // The boundary is a percentage out of 100 so use 0 as the min and 100 as the max.
921 $boundary = grade_grade::standardise_score($boundary, 0, 100, 0, 100);
923 if ($value >= $boundary) {
924 return format_string($letter);
927 return '-'; // no match? maybe '' would be more correct
932 * Returns grade options for gradebook grade category menu
934 * @param int $courseid The course ID
935 * @param bool $includenew Include option for new category at array index -1
936 * @return array of grade categories in course
938 function grade_get_categories_menu($courseid, $includenew=false) {
939 $result = array();
940 if (!$categories = grade_category::fetch_all(array('courseid'=>$courseid))) {
941 //make sure course category exists
942 if (!grade_category::fetch_course_category($courseid)) {
943 debugging('Can not create course grade category!');
944 return $result;
946 $categories = grade_category::fetch_all(array('courseid'=>$courseid));
948 foreach ($categories as $key=>$category) {
949 if ($category->is_course_category()) {
950 $result[$category->id] = get_string('uncategorised', 'grades');
951 unset($categories[$key]);
954 if ($includenew) {
955 $result[-1] = get_string('newcategory', 'grades');
957 $cats = array();
958 foreach ($categories as $category) {
959 $cats[$category->id] = $category->get_name();
961 core_collator::asort($cats);
963 return ($result+$cats);
967 * Returns the array of grade letters to be used in the supplied context
969 * @param object $context Context object or null for defaults
970 * @return array of grade_boundary (minimum) => letter_string
972 function grade_get_letters($context=null) {
973 global $DB;
975 if (empty($context)) {
976 //default grading letters
977 return array('93'=>'A', '90'=>'A-', '87'=>'B+', '83'=>'B', '80'=>'B-', '77'=>'C+', '73'=>'C', '70'=>'C-', '67'=>'D+', '60'=>'D', '0'=>'F');
980 $cache = cache::make('core', 'grade_letters');
981 $data = $cache->get($context->id);
983 if (!empty($data)) {
984 return $data;
987 $letters = array();
989 $contexts = $context->get_parent_context_ids();
990 array_unshift($contexts, $context->id);
992 foreach ($contexts as $ctxid) {
993 if ($records = $DB->get_records('grade_letters', array('contextid'=>$ctxid), 'lowerboundary DESC')) {
994 foreach ($records as $record) {
995 $letters[$record->lowerboundary] = $record->letter;
999 if (!empty($letters)) {
1000 // Cache the grade letters for this context.
1001 $cache->set($context->id, $letters);
1002 return $letters;
1006 $letters = grade_get_letters(null);
1007 // Cache the grade letters for this context.
1008 $cache->set($context->id, $letters);
1009 return $letters;
1014 * Verify new value of grade item idnumber. Checks for uniqueness of new ID numbers. Old ID numbers are kept intact.
1016 * @param string $idnumber string (with magic quotes)
1017 * @param int $courseid ID numbers are course unique only
1018 * @param grade_item $grade_item The grade item this idnumber is associated with
1019 * @param stdClass $cm used for course module idnumbers and items attached to modules
1020 * @return bool true means idnumber ok
1022 function grade_verify_idnumber($idnumber, $courseid, $grade_item=null, $cm=null) {
1023 global $DB;
1025 if ($idnumber == '') {
1026 //we allow empty idnumbers
1027 return true;
1030 // keep existing even when not unique
1031 if ($cm and $cm->idnumber == $idnumber) {
1032 if ($grade_item and $grade_item->itemnumber != 0) {
1033 // grade item with itemnumber > 0 can't have the same idnumber as the main
1034 // itemnumber 0 which is synced with course_modules
1035 return false;
1037 return true;
1038 } else if ($grade_item and $grade_item->idnumber == $idnumber) {
1039 return true;
1042 if ($DB->record_exists('course_modules', array('course'=>$courseid, 'idnumber'=>$idnumber))) {
1043 return false;
1046 if ($DB->record_exists('grade_items', array('courseid'=>$courseid, 'idnumber'=>$idnumber))) {
1047 return false;
1050 return true;
1054 * Force final grade recalculation in all course items
1056 * @param int $courseid The course ID to recalculate
1058 function grade_force_full_regrading($courseid) {
1059 global $DB;
1060 $DB->set_field('grade_items', 'needsupdate', 1, array('courseid'=>$courseid));
1064 * Forces regrading of all site grades. Used when changing site setings
1066 function grade_force_site_regrading() {
1067 global $CFG, $DB;
1068 $DB->set_field('grade_items', 'needsupdate', 1);
1072 * Recover a user's grades from grade_grades_history
1073 * @param int $userid the user ID whose grades we want to recover
1074 * @param int $courseid the relevant course
1075 * @return bool true if successful or false if there was an error or no grades could be recovered
1077 function grade_recover_history_grades($userid, $courseid) {
1078 global $CFG, $DB;
1080 if ($CFG->disablegradehistory) {
1081 debugging('Attempting to recover grades when grade history is disabled.');
1082 return false;
1085 //Were grades recovered? Flag to return.
1086 $recoveredgrades = false;
1088 //Check the user is enrolled in this course
1089 //Dont bother checking if they have a gradeable role. They may get one later so recover
1090 //whatever grades they have now just in case.
1091 $course_context = context_course::instance($courseid);
1092 if (!is_enrolled($course_context, $userid)) {
1093 debugging('Attempting to recover the grades of a user who is deleted or not enrolled. Skipping recover.');
1094 return false;
1097 //Check for existing grades for this user in this course
1098 //Recovering grades when the user already has grades can lead to duplicate indexes and bad data
1099 //In the future we could move the existing grades to the history table then recover the grades from before then
1100 $sql = "SELECT gg.id
1101 FROM {grade_grades} gg
1102 JOIN {grade_items} gi ON gi.id = gg.itemid
1103 WHERE gi.courseid = :courseid AND gg.userid = :userid";
1104 $params = array('userid' => $userid, 'courseid' => $courseid);
1105 if ($DB->record_exists_sql($sql, $params)) {
1106 debugging('Attempting to recover the grades of a user who already has grades. Skipping recover.');
1107 return false;
1108 } else {
1109 //Retrieve the user's old grades
1110 //have history ID as first column to guarantee we a unique first column
1111 $sql = "SELECT h.id, gi.itemtype, gi.itemmodule, gi.iteminstance as iteminstance, gi.itemnumber, h.source, h.itemid, h.userid, h.rawgrade, h.rawgrademax,
1112 h.rawgrademin, h.rawscaleid, h.usermodified, h.finalgrade, h.hidden, h.locked, h.locktime, h.exported, h.overridden, h.excluded, h.feedback,
1113 h.feedbackformat, h.information, h.informationformat, h.timemodified, itemcreated.tm AS timecreated
1114 FROM {grade_grades_history} h
1115 JOIN (SELECT itemid, MAX(id) AS id
1116 FROM {grade_grades_history}
1117 WHERE userid = :userid1
1118 GROUP BY itemid) maxquery ON h.id = maxquery.id AND h.itemid = maxquery.itemid
1119 JOIN {grade_items} gi ON gi.id = h.itemid
1120 JOIN (SELECT itemid, MAX(timemodified) AS tm
1121 FROM {grade_grades_history}
1122 WHERE userid = :userid2 AND action = :insertaction
1123 GROUP BY itemid) itemcreated ON itemcreated.itemid = h.itemid
1124 WHERE gi.courseid = :courseid";
1125 $params = array('userid1' => $userid, 'userid2' => $userid , 'insertaction' => GRADE_HISTORY_INSERT, 'courseid' => $courseid);
1126 $oldgrades = $DB->get_records_sql($sql, $params);
1128 //now move the old grades to the grade_grades table
1129 foreach ($oldgrades as $oldgrade) {
1130 unset($oldgrade->id);
1132 $grade = new grade_grade($oldgrade, false);//2nd arg false as dont want to try and retrieve a record from the DB
1133 $grade->insert($oldgrade->source);
1135 //dont include default empty grades created when activities are created
1136 if (!is_null($oldgrade->finalgrade) || !is_null($oldgrade->feedback)) {
1137 $recoveredgrades = true;
1142 //Some activities require manual grade synching (moving grades from the activity into the gradebook)
1143 //If the student was deleted when synching was done they may have grades in the activity that haven't been moved across
1144 grade_grab_course_grades($courseid, null, $userid);
1146 return $recoveredgrades;
1150 * Updates all final grades in course.
1152 * @param int $courseid The course ID
1153 * @param int $userid If specified try to do a quick regrading of the grades of this user only
1154 * @param object $updated_item Optional grade item to be marked for regrading. It is required if $userid is set.
1155 * @param \core\progress\base $progress If provided, will be used to update progress on this long operation.
1156 * @return array|true true if ok, array of errors if problems found. Grade item id => error message
1158 function grade_regrade_final_grades($courseid, $userid=null, $updated_item=null, $progress=null) {
1159 // This may take a very long time and extra memory.
1160 \core_php_time_limit::raise();
1161 raise_memory_limit(MEMORY_EXTRA);
1163 $course_item = grade_item::fetch_course_item($courseid);
1165 if ($progress == null) {
1166 $progress = new \core\progress\none();
1169 if ($userid) {
1170 // one raw grade updated for one user
1171 if (empty($updated_item)) {
1172 throw new \moodle_exception("cannotbenull", 'debug', '', "updated_item");
1174 if ($course_item->needsupdate) {
1175 $updated_item->force_regrading();
1176 return array($course_item->id =>'Can not do fast regrading after updating of raw grades');
1179 } else {
1180 if (!$course_item->needsupdate) {
1181 // nothing to do :-)
1182 return true;
1186 // Categories might have to run some processing before we fetch the grade items.
1187 // This gives them a final opportunity to update and mark their children to be updated.
1188 // We need to work on the children categories up to the parent ones, so that, for instance,
1189 // if a category total is updated it will be reflected in the parent category.
1190 $cats = grade_category::fetch_all(array('courseid' => $courseid));
1191 $flatcattree = array();
1192 foreach ($cats as $cat) {
1193 if (!isset($flatcattree[$cat->depth])) {
1194 $flatcattree[$cat->depth] = array();
1196 $flatcattree[$cat->depth][] = $cat;
1198 krsort($flatcattree);
1199 foreach ($flatcattree as $depth => $cats) {
1200 foreach ($cats as $cat) {
1201 $cat->pre_regrade_final_grades();
1205 $progresstotal = 0;
1206 $progresscurrent = 0;
1208 $grade_items = grade_item::fetch_all(array('courseid'=>$courseid));
1209 $depends_on = array();
1211 foreach ($grade_items as $gid=>$gitem) {
1212 if ((!empty($updated_item) and $updated_item->id == $gid) ||
1213 $gitem->is_course_item() || $gitem->is_category_item() || $gitem->is_calculated()) {
1214 $grade_items[$gid]->needsupdate = 1;
1217 // We load all dependencies of these items later we can discard some grade_items based on this.
1218 if ($grade_items[$gid]->needsupdate) {
1219 $depends_on[$gid] = $grade_items[$gid]->depends_on();
1220 $progresstotal++;
1224 $progress->start_progress('regrade_course', $progresstotal);
1226 $errors = array();
1227 $finalids = array();
1228 $updatedids = array();
1229 $gids = array_keys($grade_items);
1230 $failed = 0;
1232 while (count($finalids) < count($gids)) { // work until all grades are final or error found
1233 $count = 0;
1234 foreach ($gids as $gid) {
1235 if (in_array($gid, $finalids)) {
1236 continue; // already final
1239 if (!$grade_items[$gid]->needsupdate) {
1240 $finalids[] = $gid; // we can make it final - does not need update
1241 continue;
1243 $thisprogress = $progresstotal;
1244 foreach ($grade_items as $item) {
1245 if ($item->needsupdate) {
1246 $thisprogress--;
1249 // Clip between $progresscurrent and $progresstotal.
1250 $thisprogress = max(min($thisprogress, $progresstotal), $progresscurrent);
1251 $progress->progress($thisprogress);
1252 $progresscurrent = $thisprogress;
1254 foreach ($depends_on[$gid] as $did) {
1255 if (!in_array($did, $finalids)) {
1256 // This item depends on something that is not yet in finals array.
1257 continue 2;
1261 // If this grade item has no dependancy with any updated item at all, then remove it from being recalculated.
1263 // When we get here, all of this grade item's decendents are marked as final so they would be marked as updated too
1264 // if they would have been regraded. We don't need to regrade items which dependants (not only the direct ones
1265 // but any dependant in the cascade) have not been updated.
1267 // If $updated_item was specified we discard the grade items that do not depend on it or on any grade item that
1268 // depend on $updated_item.
1270 // Here we check to see if the direct decendants are marked as updated.
1271 if (!empty($updated_item) && $gid != $updated_item->id && !in_array($updated_item->id, $depends_on[$gid])) {
1273 // We need to ensure that none of this item's dependencies have been updated.
1274 // If we find that one of the direct decendants of this grade item is marked as updated then this
1275 // grade item needs to be recalculated and marked as updated.
1276 // Being marked as updated is done further down in the code.
1278 $updateddependencies = false;
1279 foreach ($depends_on[$gid] as $dependency) {
1280 if (in_array($dependency, $updatedids)) {
1281 $updateddependencies = true;
1282 break;
1285 if ($updateddependencies === false) {
1286 // If no direct descendants are marked as updated, then we don't need to update this grade item. We then mark it
1287 // as final.
1288 $count++;
1289 $finalids[] = $gid;
1290 continue;
1294 // Let's update, calculate or aggregate.
1295 $result = $grade_items[$gid]->regrade_final_grades($userid, $progress);
1297 if ($result === true) {
1299 // We should only update the database if we regraded all users.
1300 if (empty($userid)) {
1301 $grade_items[$gid]->regrading_finished();
1302 // Do the locktime item locking.
1303 $grade_items[$gid]->check_locktime();
1304 } else {
1305 $grade_items[$gid]->needsupdate = 0;
1307 $count++;
1308 $finalids[] = $gid;
1309 $updatedids[] = $gid;
1311 } else {
1312 $grade_items[$gid]->force_regrading();
1313 $errors[$gid] = $result;
1317 if ($count == 0) {
1318 $failed++;
1319 } else {
1320 $failed = 0;
1323 if ($failed > 1) {
1324 foreach($gids as $gid) {
1325 if (in_array($gid, $finalids)) {
1326 continue; // this one is ok
1328 $grade_items[$gid]->force_regrading();
1329 if (!empty($grade_items[$gid]->calculation) && empty($errors[$gid])) {
1330 $itemname = $grade_items[$gid]->get_name();
1331 $errors[$gid] = get_string('errorcalculationbroken', 'grades', $itemname);
1334 break; // Found error.
1337 $progress->end_progress();
1339 if (count($errors) == 0) {
1340 if (empty($userid)) {
1341 // do the locktime locking of grades, but only when doing full regrading
1342 grade_grade::check_locktime_all($gids);
1344 return true;
1345 } else {
1346 return $errors;
1351 * Refetches grade data from course activities
1353 * @param int $courseid The course ID
1354 * @param string $modname Limit the grade fetch to a single module type. For example 'forum'
1355 * @param int $userid limit the grade fetch to a single user
1357 function grade_grab_course_grades($courseid, $modname=null, $userid=0) {
1358 global $CFG, $DB;
1360 if ($modname) {
1361 $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname
1362 FROM {".$modname."} a, {course_modules} cm, {modules} m
1363 WHERE m.name=:modname AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=:courseid";
1364 $params = array('modname'=>$modname, 'courseid'=>$courseid);
1366 if ($modinstances = $DB->get_records_sql($sql, $params)) {
1367 foreach ($modinstances as $modinstance) {
1368 grade_update_mod_grades($modinstance, $userid);
1371 return;
1374 if (!$mods = core_component::get_plugin_list('mod') ) {
1375 throw new \moodle_exception('nomodules', 'debug');
1378 foreach ($mods as $mod => $fullmod) {
1379 if ($mod == 'NEWMODULE') { // Someone has unzipped the template, ignore it
1380 continue;
1383 // include the module lib once
1384 if (file_exists($fullmod.'/lib.php')) {
1385 // get all instance of the activity
1386 $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname
1387 FROM {".$mod."} a, {course_modules} cm, {modules} m
1388 WHERE m.name=:mod AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=:courseid";
1389 $params = array('mod'=>$mod, 'courseid'=>$courseid);
1391 if ($modinstances = $DB->get_records_sql($sql, $params)) {
1392 foreach ($modinstances as $modinstance) {
1393 grade_update_mod_grades($modinstance, $userid);
1401 * Force full update of module grades in central gradebook
1403 * @param object $modinstance Module object with extra cmidnumber and modname property
1404 * @param int $userid Optional user ID if limiting the update to a single user
1405 * @return bool True if success
1407 function grade_update_mod_grades($modinstance, $userid=0) {
1408 global $CFG, $DB;
1410 $fullmod = $CFG->dirroot.'/mod/'.$modinstance->modname;
1411 if (!file_exists($fullmod.'/lib.php')) {
1412 debugging('missing lib.php file in module ' . $modinstance->modname);
1413 return false;
1415 include_once($fullmod.'/lib.php');
1417 $updateitemfunc = $modinstance->modname.'_grade_item_update';
1418 $updategradesfunc = $modinstance->modname.'_update_grades';
1420 if (function_exists($updategradesfunc) and function_exists($updateitemfunc)) {
1421 //new grading supported, force updating of grades
1422 $updateitemfunc($modinstance);
1423 $updategradesfunc($modinstance, $userid);
1424 } else if (function_exists($updategradesfunc) xor function_exists($updateitemfunc)) {
1425 // Module does not support grading?
1426 debugging("You have declared one of $updateitemfunc and $updategradesfunc but not both. " .
1427 "This will cause broken behaviour.", DEBUG_DEVELOPER);
1430 return true;
1434 * Remove grade letters for given context
1436 * @param context $context The context
1437 * @param bool $showfeedback If true a success notification will be displayed
1439 function remove_grade_letters($context, $showfeedback) {
1440 global $DB, $OUTPUT;
1442 $strdeleted = get_string('deleted');
1444 $records = $DB->get_records('grade_letters', array('contextid' => $context->id));
1445 foreach ($records as $record) {
1446 $DB->delete_records('grade_letters', array('id' => $record->id));
1447 // Trigger the letter grade deleted event.
1448 $event = \core\event\grade_letter_deleted::create(array(
1449 'objectid' => $record->id,
1450 'context' => $context,
1452 $event->trigger();
1454 if ($showfeedback) {
1455 echo $OUTPUT->notification($strdeleted.' - '.get_string('letters', 'grades'), 'notifysuccess');
1458 $cache = cache::make('core', 'grade_letters');
1459 $cache->delete($context->id);
1463 * Remove all grade related course data
1464 * Grade history is kept
1466 * @param int $courseid The course ID
1467 * @param bool $showfeedback If true success notifications will be displayed
1469 function remove_course_grades($courseid, $showfeedback) {
1470 global $DB, $OUTPUT;
1472 $fs = get_file_storage();
1473 $strdeleted = get_string('deleted');
1475 $course_category = grade_category::fetch_course_category($courseid);
1476 $course_category->delete('coursedelete');
1477 $fs->delete_area_files(context_course::instance($courseid)->id, 'grade', 'feedback');
1478 if ($showfeedback) {
1479 echo $OUTPUT->notification($strdeleted.' - '.get_string('grades', 'grades').', '.get_string('items', 'grades').', '.get_string('categories', 'grades'), 'notifysuccess');
1482 if ($outcomes = grade_outcome::fetch_all(array('courseid'=>$courseid))) {
1483 foreach ($outcomes as $outcome) {
1484 $outcome->delete('coursedelete');
1487 $DB->delete_records('grade_outcomes_courses', array('courseid'=>$courseid));
1488 if ($showfeedback) {
1489 echo $OUTPUT->notification($strdeleted.' - '.get_string('outcomes', 'grades'), 'notifysuccess');
1492 if ($scales = grade_scale::fetch_all(array('courseid'=>$courseid))) {
1493 foreach ($scales as $scale) {
1494 $scale->delete('coursedelete');
1497 if ($showfeedback) {
1498 echo $OUTPUT->notification($strdeleted.' - '.get_string('scales'), 'notifysuccess');
1501 $DB->delete_records('grade_settings', array('courseid'=>$courseid));
1502 if ($showfeedback) {
1503 echo $OUTPUT->notification($strdeleted.' - '.get_string('settings', 'grades'), 'notifysuccess');
1508 * Called when course category is deleted
1509 * Cleans the gradebook of associated data
1511 * @param int $categoryid The course category id
1512 * @param int $newparentid If empty everything is deleted. Otherwise the ID of the category where content moved
1513 * @param bool $showfeedback print feedback
1515 function grade_course_category_delete($categoryid, $newparentid, $showfeedback) {
1516 global $DB;
1518 $context = context_coursecat::instance($categoryid);
1519 $records = $DB->get_records('grade_letters', array('contextid' => $context->id));
1520 foreach ($records as $record) {
1521 $DB->delete_records('grade_letters', array('id' => $record->id));
1522 // Trigger the letter grade deleted event.
1523 $event = \core\event\grade_letter_deleted::create(array(
1524 'objectid' => $record->id,
1525 'context' => $context,
1527 $event->trigger();
1532 * Does gradebook cleanup when a module is uninstalled
1533 * Deletes all associated grade items
1535 * @param string $modname The grade item module name to remove. For example 'forum'
1537 function grade_uninstalled_module($modname) {
1538 global $CFG, $DB;
1540 $sql = "SELECT *
1541 FROM {grade_items}
1542 WHERE itemtype='mod' AND itemmodule=?";
1544 // go all items for this module and delete them including the grades
1545 $rs = $DB->get_recordset_sql($sql, array($modname));
1546 foreach ($rs as $item) {
1547 $grade_item = new grade_item($item, false);
1548 $grade_item->delete('moduninstall');
1550 $rs->close();
1554 * Deletes all of a user's grade data from gradebook
1556 * @param int $userid The user whose grade data should be deleted
1558 function grade_user_delete($userid) {
1559 if ($grades = grade_grade::fetch_all(array('userid'=>$userid))) {
1560 foreach ($grades as $grade) {
1561 $grade->delete('userdelete');
1567 * Purge course data when user unenrolls from a course
1569 * @param int $courseid The ID of the course the user has unenrolled from
1570 * @param int $userid The ID of the user unenrolling
1572 function grade_user_unenrol($courseid, $userid) {
1573 if ($items = grade_item::fetch_all(array('courseid'=>$courseid))) {
1574 foreach ($items as $item) {
1575 if ($grades = grade_grade::fetch_all(array('userid'=>$userid, 'itemid'=>$item->id))) {
1576 foreach ($grades as $grade) {
1577 $grade->delete('userdelete');
1585 * Reset all course grades, refetch from the activities and recalculate
1587 * @param int $courseid The course to reset
1588 * @return bool success
1590 function grade_course_reset($courseid) {
1592 // no recalculations
1593 grade_force_full_regrading($courseid);
1595 $grade_items = grade_item::fetch_all(array('courseid'=>$courseid));
1596 foreach ($grade_items as $gid=>$grade_item) {
1597 $grade_item->delete_all_grades('reset');
1600 //refetch all grades
1601 grade_grab_course_grades($courseid);
1603 // recalculate all grades
1604 grade_regrade_final_grades($courseid);
1605 return true;
1609 * Convert a number to 5 decimal point float, null db compatible format
1610 * (we need this to decide if db value changed)
1612 * @param float|null $number The number to convert
1613 * @return float|null float or null
1615 function grade_floatval(?float $number) {
1616 if (is_null($number)) {
1617 return null;
1619 // we must round to 5 digits to get the same precision as in 10,5 db fields
1620 // note: db rounding for 10,5 is different from php round() function
1621 return round($number, 5);
1625 * Compare two float numbers safely. Uses 5 decimals php precision using {@link grade_floatval()}. Nulls accepted too.
1626 * Used for determining if a database update is required
1628 * @param float|null $f1 Float one to compare
1629 * @param float|null $f2 Float two to compare
1630 * @return bool True if the supplied values are different
1632 function grade_floats_different(?float $f1, ?float $f2): bool {
1633 // note: db rounding for 10,5 is different from php round() function
1634 return (grade_floatval($f1) !== grade_floatval($f2));
1638 * Compare two float numbers safely. Uses 5 decimals php precision using {@link grade_floatval()}
1640 * Do not use rounding for 10,5 at the database level as the results may be
1641 * different from php round() function.
1643 * @since Moodle 2.0
1644 * @param float|null $f1 Float one to compare
1645 * @param float|null $f2 Float two to compare
1646 * @return bool True if the values should be considered as the same grades
1648 function grade_floats_equal(?float $f1, ?float $f2): bool {
1649 return (grade_floatval($f1) === grade_floatval($f2));
1653 * Get the most appropriate grade date for a grade item given the user that the grade relates to.
1655 * @param \stdClass $grade
1656 * @param \stdClass $user
1657 * @return int|null
1659 function grade_get_date_for_user_grade(\stdClass $grade, \stdClass $user): ?int {
1660 // The `datesubmitted` is the time that the grade was created.
1661 // The `dategraded` is the time that it was modified or overwritten.
1662 // If the grade was last modified by the user themselves use the date graded.
1663 // Otherwise use date submitted.
1664 if ($grade->usermodified == $user->id || empty($grade->datesubmitted)) {
1665 return $grade->dategraded;
1666 } else {
1667 return $grade->datesubmitted;