2 // This file is part of Moodle - http://moodle.org/
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.
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/>.
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();
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 /////////////////////////////////////////////////////////////////////
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.
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 * @return int Returns GRADE_UPDATE_OK, GRADE_UPDATE_FAILED, GRADE_UPDATE_MULTIPLE or GRADE_UPDATE_ITEM_LOCKED
63 function grade_update($source, $courseid, $itemtype, $itemmodule, $iteminstance, $itemnumber, $grades=NULL, $itemdetails=NULL) {
64 global $USER, $CFG, $DB;
66 // only following grade_item properties can be changed in this function
67 $allowed = array('itemname', 'idnumber', 'gradetype', 'grademax', 'grademin', 'scaleid', 'multfactor', 'plusfactor', 'deleted', 'hidden');
68 // list of 10,5 numeric fields
69 $floats = array('grademin', 'grademax', 'multfactor', 'plusfactor');
71 // grade item identification
72 $params = compact('courseid', 'itemtype', 'itemmodule', 'iteminstance', 'itemnumber');
74 if (is_null($courseid) or is_null($itemtype)) {
75 debugging('Missing courseid or itemtype');
76 return GRADE_UPDATE_FAILED
;
79 if (!$grade_items = grade_item
::fetch_all($params)) {
83 } else if (count($grade_items) == 1){
84 $grade_item = reset($grade_items);
85 unset($grade_items); //release memory
88 debugging('Found more than one grade item');
89 return GRADE_UPDATE_MULTIPLE
;
92 if (!empty($itemdetails['deleted'])) {
94 if ($grade_item->delete($source)) {
95 return GRADE_UPDATE_OK
;
97 return GRADE_UPDATE_FAILED
;
100 return GRADE_UPDATE_OK
;
103 /// Create or update the grade_item if needed
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)) {
122 if ($k == 'gradetype' and $v == GRADE_TYPE_NONE
) {
123 // no grade item needed!
124 return GRADE_UPDATE_OK
;
129 $grade_item = new grade_item($params);
130 $grade_item->insert();
133 if ($grade_item->is_locked()) {
134 // no notice() here, test returned value instead!
135 return GRADE_UPDATE_ITEM_LOCKED
;
139 $itemdetails = (array)$itemdetails;
141 foreach ($itemdetails as $k=>$v) {
142 if (!in_array($k, $allowed)) {
146 if (in_array($k, $floats)) {
147 if (grade_floats_different($grade_item->{$k}, $v)) {
148 $grade_item->{$k} = $v;
153 if ($grade_item->{$k} != $v) {
154 $grade_item->{$k} = $v;
160 $grade_item->update();
165 /// reset grades if requested
166 if (!empty($itemdetails['reset'])) {
167 $grade_item->delete_all_grades('reset');
168 return GRADE_UPDATE_OK
;
171 /// Some extra checks
172 // do we use grading?
173 if ($grade_item->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);
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) {
198 if (empty($g['userid']) or $k != $g['userid']) {
199 debugging('Incorrect grade array index, must be user id! Grade ignored.');
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'] = $grade_item->id
;
212 $sql = "SELECT * FROM {grade_grades} WHERE itemid = :gid AND userid $uids";
215 $sql = "SELECT * FROM {grade_grades} WHERE itemid = :gid";
216 $params = array('gid'=>$grade_item->id
);
219 $rs = $DB->get_recordset_sql($sql, $params);
223 while (count($grades) > 0) {
227 foreach ($rs as $gd) {
229 $userid = $gd->userid
;
230 if (!isset($grades[$userid])) {
231 // this grade not requested, continue
234 // existing grade requested
235 $grade = $grades[$userid];
236 $grade_grade = new grade_grade($gd, false);
237 unset($grades[$userid]);
241 if (is_null($grade_grade)) {
242 if (count($grades) == 0) {
243 // no more grades to process
247 $grade = reset($grades);
248 $userid = $grade['userid'];
249 $grade_grade = new grade_grade(array('itemid'=>$grade_item->id
, 'userid'=>$userid), false);
250 $grade_grade->load_optional_fields(); // add feedback and info too
251 unset($grades[$userid]);
256 $feedbackformat = FORMAT_MOODLE
;
258 $usermodified = $USER->id
;
259 $datesubmitted = 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 (!$grade_item->update_raw_grade($userid, $rawgrade, $source, $feedback, $feedbackformat, $usermodified,
292 $dategraded, $datesubmitted, $grade_grade, $feedbackfiles)) {
302 return GRADE_UPDATE_OK
;
304 return GRADE_UPDATE_FAILED
;
309 * Updates a user's outcomes. Manual outcomes can not be updated.
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))) {
324 foreach ($items as $item) {
325 if (!array_key_exists($item->itemnumber
, $data)) {
328 $grade = $data[$item->itemnumber
] < 1 ?
null : $data[$item->itemnumber
];
329 $result = ($item->update_final_grade($userid, $grade, $source) && $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) {
356 $grade_items = grade_item
::fetch_all(array('courseid' => $courseid));
358 list($sql, $params) = $DB->get_in_or_equal(array_keys($grade_items), SQL_PARAMS_NAMED
, 'gi');
359 $gradecount = $DB->count_records_select('grade_grades', 'itemid ' . $sql, $params);
361 // This figure may seem arbitrary, but after analysis it seems that 100 grade_grades can be calculated in ~= 0.5 seconds.
362 // Any longer than this and we want to show the progress bar.
363 return $gradecount > 100;
367 * Check whether regarding of final grades is required and, if so, perform the regrade.
369 * If the regrade is expected to be time consuming (see grade_needs_regrade_progress_bar), then this
370 * function will output the progress bar, and redirect to the current PAGE->url after regrading
371 * completes. Otherwise the regrading will happen immediately and the page will be loaded as per
374 * A callback may be specified, which is called if regrading has taken place.
375 * The callback may optionally return a URL which will be redirected to when the progress bar is present.
377 * @param stdClass $course The course to regrade
378 * @param callable $callback A function to call if regrading took place
379 * @return moodle_url The URL to redirect to if redirecting
381 function grade_regrade_final_grades_if_required($course, callable
$callback = null) {
382 global $PAGE, $OUTPUT;
384 if (!grade_needs_regrade_final_grades($course->id
)) {
388 if (grade_needs_regrade_progress_bar($course->id
)) {
389 $PAGE->set_heading($course->fullname
);
390 echo $OUTPUT->header();
391 echo $OUTPUT->heading(get_string('recalculatinggrades', 'grades'));
392 $progress = new \core\progress\
display(true);
393 $status = grade_regrade_final_grades($course->id
, null, null, $progress);
395 // Show regrade errors and set the course to no longer needing regrade (stop endless loop).
396 if (is_array($status)) {
397 foreach ($status as $error) {
398 $errortext = new \core\output\notification
($error, \core\output\notification
::NOTIFY_ERROR
);
399 echo $OUTPUT->render($errortext);
401 $courseitem = grade_item
::fetch_course_item($course->id
);
402 $courseitem->regrading_finished();
407 $url = call_user_func($callback);
414 echo $OUTPUT->continue_button($url);
415 echo $OUTPUT->footer();
418 $result = grade_regrade_final_grades($course->id
);
420 call_user_func($callback);
427 * Returns grading information for given activity, optionally with user grades
428 * Manual, course or category items can not be queried.
431 * @param int $courseid ID of course
432 * @param string $itemtype Type of grade item. For example, 'mod' or 'block'
433 * @param string $itemmodule More specific then $itemtype. For example, 'forum' or 'quiz'. May be NULL for some item types
434 * @param int $iteminstance ID of the item module
435 * @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
436 * @return array Array of grade information objects (scaleid, name, grade and locked status, etc.) indexed with itemnumbers
438 function grade_get_grades($courseid, $itemtype, $itemmodule, $iteminstance, $userid_or_ids=null) {
441 $return = new stdClass();
442 $return->items
= array();
443 $return->outcomes
= array();
445 $course_item = grade_item
::fetch_course_item($courseid);
446 $needsupdate = array();
447 if ($course_item->needsupdate
) {
448 $result = grade_regrade_final_grades($courseid);
449 if ($result !== true) {
450 $needsupdate = array_keys($result);
454 if ($grade_items = grade_item
::fetch_all(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid))) {
455 foreach ($grade_items as $grade_item) {
456 $decimalpoints = null;
458 if (empty($grade_item->outcomeid
)) {
459 // prepare information about grade item
460 $item = new stdClass();
461 $item->id
= $grade_item->id
;
462 $item->itemnumber
= $grade_item->itemnumber
;
463 $item->itemtype
= $grade_item->itemtype
;
464 $item->itemmodule
= $grade_item->itemmodule
;
465 $item->iteminstance
= $grade_item->iteminstance
;
466 $item->scaleid
= $grade_item->scaleid
;
467 $item->name
= $grade_item->get_name();
468 $item->grademin
= $grade_item->grademin
;
469 $item->grademax
= $grade_item->grademax
;
470 $item->gradepass
= $grade_item->gradepass
;
471 $item->locked
= $grade_item->is_locked();
472 $item->hidden
= $grade_item->is_hidden();
473 $item->grades
= array();
475 switch ($grade_item->gradetype
) {
476 case GRADE_TYPE_NONE
:
479 case GRADE_TYPE_VALUE
:
483 case GRADE_TYPE_TEXT
:
487 $item->gradepass
= 0;
491 if (empty($userid_or_ids)) {
494 } else if (is_array($userid_or_ids)) {
495 $userids = $userid_or_ids;
498 $userids = array($userid_or_ids);
502 $grade_grades = grade_grade
::fetch_users_grades($grade_item, $userids, true);
503 foreach ($userids as $userid) {
504 $grade_grades[$userid]->grade_item
=& $grade_item;
506 $grade = new stdClass();
507 $grade->grade
= $grade_grades[$userid]->finalgrade
;
508 $grade->locked
= $grade_grades[$userid]->is_locked();
509 $grade->hidden
= $grade_grades[$userid]->is_hidden();
510 $grade->overridden
= $grade_grades[$userid]->overridden
;
511 $grade->feedback
= $grade_grades[$userid]->feedback
;
512 $grade->feedbackformat
= $grade_grades[$userid]->feedbackformat
;
513 $grade->usermodified
= $grade_grades[$userid]->usermodified
;
514 $grade->datesubmitted
= $grade_grades[$userid]->get_datesubmitted();
515 $grade->dategraded
= $grade_grades[$userid]->get_dategraded();
517 // create text representation of grade
518 if ($grade_item->gradetype
== GRADE_TYPE_TEXT
or $grade_item->gradetype
== GRADE_TYPE_NONE
) {
519 $grade->grade
= null;
520 $grade->str_grade
= '-';
521 $grade->str_long_grade
= $grade->str_grade
;
523 } else if (in_array($grade_item->id
, $needsupdate)) {
524 $grade->grade
= false;
525 $grade->str_grade
= get_string('error');
526 $grade->str_long_grade
= $grade->str_grade
;
528 } else if (is_null($grade->grade
)) {
529 $grade->str_grade
= '-';
530 $grade->str_long_grade
= $grade->str_grade
;
533 $grade->str_grade
= grade_format_gradevalue($grade->grade
, $grade_item);
534 if ($grade_item->gradetype
== GRADE_TYPE_SCALE
or $grade_item->get_displaytype() != GRADE_DISPLAY_TYPE_REAL
) {
535 $grade->str_long_grade
= $grade->str_grade
;
538 $a->grade
= $grade->str_grade
;
539 $a->max
= grade_format_gradevalue($grade_item->grademax
, $grade_item);
540 $grade->str_long_grade
= get_string('gradelong', 'grades', $a);
544 // create html representation of feedback
545 if (is_null($grade->feedback
)) {
546 $grade->str_feedback
= '';
548 $feedback = file_rewrite_pluginfile_urls(
551 $grade_grades[$userid]->get_context()->id
,
552 GRADE_FILE_COMPONENT
,
553 GRADE_FEEDBACK_FILEAREA
,
554 $grade_grades[$userid]->id
557 $grade->str_feedback
= format_text($feedback, $grade->feedbackformat
,
558 ['context' => $grade_grades[$userid]->get_context()]);
561 $item->grades
[$userid] = $grade;
564 $return->items
[$grade_item->itemnumber
] = $item;
567 if (!$grade_outcome = grade_outcome
::fetch(array('id'=>$grade_item->outcomeid
))) {
568 debugging('Incorect outcomeid found');
573 $outcome = new stdClass();
574 $outcome->id
= $grade_item->id
;
575 $outcome->itemnumber
= $grade_item->itemnumber
;
576 $outcome->itemtype
= $grade_item->itemtype
;
577 $outcome->itemmodule
= $grade_item->itemmodule
;
578 $outcome->iteminstance
= $grade_item->iteminstance
;
579 $outcome->scaleid
= $grade_outcome->scaleid
;
580 $outcome->name
= $grade_outcome->get_name();
581 $outcome->locked
= $grade_item->is_locked();
582 $outcome->hidden
= $grade_item->is_hidden();
584 if (empty($userid_or_ids)) {
586 } else if (is_array($userid_or_ids)) {
587 $userids = $userid_or_ids;
589 $userids = array($userid_or_ids);
593 $grade_grades = grade_grade
::fetch_users_grades($grade_item, $userids, true);
594 foreach ($userids as $userid) {
595 $grade_grades[$userid]->grade_item
=& $grade_item;
597 $grade = new stdClass();
598 $grade->grade
= $grade_grades[$userid]->finalgrade
;
599 $grade->locked
= $grade_grades[$userid]->is_locked();
600 $grade->hidden
= $grade_grades[$userid]->is_hidden();
601 $grade->feedback
= $grade_grades[$userid]->feedback
;
602 $grade->feedbackformat
= $grade_grades[$userid]->feedbackformat
;
603 $grade->usermodified
= $grade_grades[$userid]->usermodified
;
604 $grade->datesubmitted
= $grade_grades[$userid]->get_datesubmitted();
605 $grade->dategraded
= $grade_grades[$userid]->get_dategraded();
607 // create text representation of grade
608 if (in_array($grade_item->id
, $needsupdate)) {
609 $grade->grade
= false;
610 $grade->str_grade
= get_string('error');
612 } else if (is_null($grade->grade
)) {
614 $grade->str_grade
= get_string('nooutcome', 'grades');
617 $grade->grade
= (int)$grade->grade
;
618 $scale = $grade_item->load_scale();
619 $grade->str_grade
= format_string($scale->scale_items
[(int)$grade->grade
-1]);
622 // create html representation of feedback
623 if (is_null($grade->feedback
)) {
624 $grade->str_feedback
= '';
626 $grade->str_feedback
= format_text($grade->feedback
, $grade->feedbackformat
);
629 $outcome->grades
[$userid] = $grade;
633 if (isset($return->outcomes
[$grade_item->itemnumber
])) {
634 // itemnumber duplicates - lets fix them!
635 $newnumber = $grade_item->itemnumber +
1;
636 while(grade_item
::fetch(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid, 'itemnumber'=>$newnumber))) {
639 $outcome->itemnumber
= $newnumber;
640 $grade_item->itemnumber
= $newnumber;
641 $grade_item->update('system');
644 $return->outcomes
[$grade_item->itemnumber
] = $outcome;
650 // sort results using itemnumbers
651 ksort($return->items
, SORT_NUMERIC
);
652 ksort($return->outcomes
, SORT_NUMERIC
);
657 ///////////////////////////////////////////////////////////////////
658 ///// End of public API for communication with modules/blocks /////
659 ///////////////////////////////////////////////////////////////////
663 ///////////////////////////////////////////////////////////////////
664 ///// Internal API: used by gradebook plugins and Moodle core /////
665 ///////////////////////////////////////////////////////////////////
668 * Returns a course gradebook setting
670 * @param int $courseid
671 * @param string $name of setting, maybe null if reset only
672 * @param string $default value to return if setting is not found
673 * @param bool $resetcache force reset of internal static cache
674 * @return string value of the setting, $default if setting not found, NULL if supplied $name is null
676 function grade_get_setting($courseid, $name, $default=null, $resetcache=false) {
679 static $cache = array();
681 if ($resetcache or !array_key_exists($courseid, $cache)) {
682 $cache[$courseid] = array();
684 } else if (is_null($name)) {
687 } else if (array_key_exists($name, $cache[$courseid])) {
688 return $cache[$courseid][$name];
691 if (!$data = $DB->get_record('grade_settings', array('courseid'=>$courseid, 'name'=>$name))) {
694 $result = $data->value
;
697 if (is_null($result)) {
701 $cache[$courseid][$name] = $result;
706 * Returns all course gradebook settings as object properties
708 * @param int $courseid
711 function grade_get_settings($courseid) {
714 $settings = new stdClass();
715 $settings->id
= $courseid;
717 if ($records = $DB->get_records('grade_settings', array('courseid'=>$courseid))) {
718 foreach ($records as $record) {
719 $settings->{$record->name
} = $record->value
;
727 * Add, update or delete a course gradebook setting
729 * @param int $courseid The course ID
730 * @param string $name Name of the setting
731 * @param string $value Value of the setting. NULL means delete the setting.
733 function grade_set_setting($courseid, $name, $value) {
736 if (is_null($value)) {
737 $DB->delete_records('grade_settings', array('courseid'=>$courseid, 'name'=>$name));
739 } else if (!$existing = $DB->get_record('grade_settings', array('courseid'=>$courseid, 'name'=>$name))) {
740 $data = new stdClass();
741 $data->courseid
= $courseid;
743 $data->value
= $value;
744 $DB->insert_record('grade_settings', $data);
747 $data = new stdClass();
748 $data->id
= $existing->id
;
749 $data->value
= $value;
750 $DB->update_record('grade_settings', $data);
753 grade_get_setting($courseid, null, null, true); // reset the cache
757 * Returns string representation of grade value
759 * @param float $value The grade value
760 * @param object $grade_item Grade item object passed by reference to prevent scale reloading
761 * @param bool $localized use localised decimal separator
762 * @param int $displaytype type of display. For example GRADE_DISPLAY_TYPE_REAL, GRADE_DISPLAY_TYPE_PERCENTAGE, GRADE_DISPLAY_TYPE_LETTER
763 * @param int $decimals The number of decimal places when displaying float values
766 function grade_format_gradevalue($value, &$grade_item, $localized=true, $displaytype=null, $decimals=null) {
767 if ($grade_item->gradetype
== GRADE_TYPE_NONE
or $grade_item->gradetype
== GRADE_TYPE_TEXT
) {
772 if (is_null($value)) {
776 if ($grade_item->gradetype
!= GRADE_TYPE_VALUE
and $grade_item->gradetype
!= GRADE_TYPE_SCALE
) {
781 if (is_null($displaytype)) {
782 $displaytype = $grade_item->get_displaytype();
785 if (is_null($decimals)) {
786 $decimals = $grade_item->get_decimals();
789 switch ($displaytype) {
790 case GRADE_DISPLAY_TYPE_REAL
:
791 return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized);
793 case GRADE_DISPLAY_TYPE_PERCENTAGE
:
794 return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized);
796 case GRADE_DISPLAY_TYPE_LETTER
:
797 return grade_format_gradevalue_letter($value, $grade_item);
799 case GRADE_DISPLAY_TYPE_REAL_PERCENTAGE
:
800 return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ' (' .
801 grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ')';
803 case GRADE_DISPLAY_TYPE_REAL_LETTER
:
804 return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ' (' .
805 grade_format_gradevalue_letter($value, $grade_item) . ')';
807 case GRADE_DISPLAY_TYPE_PERCENTAGE_REAL
:
808 return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ' (' .
809 grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ')';
811 case GRADE_DISPLAY_TYPE_LETTER_REAL
:
812 return grade_format_gradevalue_letter($value, $grade_item) . ' (' .
813 grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ')';
815 case GRADE_DISPLAY_TYPE_LETTER_PERCENTAGE
:
816 return grade_format_gradevalue_letter($value, $grade_item) . ' (' .
817 grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ')';
819 case GRADE_DISPLAY_TYPE_PERCENTAGE_LETTER
:
820 return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ' (' .
821 grade_format_gradevalue_letter($value, $grade_item) . ')';
828 * Returns a float representation of a grade value
830 * @param float $value The grade value
831 * @param object $grade_item Grade item object
832 * @param int $decimals The number of decimal places
833 * @param bool $localized use localised decimal separator
836 function grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) {
837 if ($grade_item->gradetype
== GRADE_TYPE_SCALE
) {
838 if (!$scale = $grade_item->load_scale()) {
839 return get_string('error');
842 $value = $grade_item->bounded_grade($value);
843 return format_string($scale->scale_items
[$value-1]);
846 return format_float($value, $decimals, $localized);
851 * Returns a percentage representation of a grade value
853 * @param float $value The grade value
854 * @param object $grade_item Grade item object
855 * @param int $decimals The number of decimal places
856 * @param bool $localized use localised decimal separator
859 function grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) {
860 $min = $grade_item->grademin
;
861 $max = $grade_item->grademax
;
865 $value = $grade_item->bounded_grade($value);
866 $percentage = (($value-$min)*100)/($max-$min);
867 return format_float($percentage, $decimals, $localized).' %';
871 * Returns a letter grade representation of a grade value
872 * The array of grade letters used is produced by {@link grade_get_letters()} using the course context
874 * @param float $value The grade value
875 * @param object $grade_item Grade item object
878 function grade_format_gradevalue_letter($value, $grade_item) {
880 $context = context_course
::instance($grade_item->courseid
, IGNORE_MISSING
);
881 if (!$letters = grade_get_letters($context)) {
882 return ''; // no letters??
885 if (is_null($value)) {
889 $value = grade_grade
::standardise_score($value, $grade_item->grademin
, $grade_item->grademax
, 0, 100);
890 $value = bounded_number(0, $value, 100); // just in case
892 $gradebookcalculationsfreeze = 'gradebook_calculations_freeze_' . $grade_item->courseid
;
894 foreach ($letters as $boundary => $letter) {
895 if (property_exists($CFG, $gradebookcalculationsfreeze) && (int)$CFG->{$gradebookcalculationsfreeze} <= 20160518) {
898 // The boundary is a percentage out of 100 so use 0 as the min and 100 as the max.
899 $boundary = grade_grade
::standardise_score($boundary, 0, 100, 0, 100);
901 if ($value >= $boundary) {
902 return format_string($letter);
905 return '-'; // no match? maybe '' would be more correct
910 * Returns grade options for gradebook grade category menu
912 * @param int $courseid The course ID
913 * @param bool $includenew Include option for new category at array index -1
914 * @return array of grade categories in course
916 function grade_get_categories_menu($courseid, $includenew=false) {
918 if (!$categories = grade_category
::fetch_all(array('courseid'=>$courseid))) {
919 //make sure course category exists
920 if (!grade_category
::fetch_course_category($courseid)) {
921 debugging('Can not create course grade category!');
924 $categories = grade_category
::fetch_all(array('courseid'=>$courseid));
926 foreach ($categories as $key=>$category) {
927 if ($category->is_course_category()) {
928 $result[$category->id
] = get_string('uncategorised', 'grades');
929 unset($categories[$key]);
933 $result[-1] = get_string('newcategory', 'grades');
936 foreach ($categories as $category) {
937 $cats[$category->id
] = $category->get_name();
939 core_collator
::asort($cats);
941 return ($result+
$cats);
945 * Returns the array of grade letters to be used in the supplied context
947 * @param object $context Context object or null for defaults
948 * @return array of grade_boundary (minimum) => letter_string
950 function grade_get_letters($context=null) {
953 if (empty($context)) {
954 //default grading letters
955 return array('93'=>'A', '90'=>'A-', '87'=>'B+', '83'=>'B', '80'=>'B-', '77'=>'C+', '73'=>'C', '70'=>'C-', '67'=>'D+', '60'=>'D', '0'=>'F');
958 static $cache = array();
960 if (array_key_exists($context->id
, $cache)) {
961 return $cache[$context->id
];
964 if (count($cache) > 100) {
965 $cache = array(); // cache size limit
970 $contexts = $context->get_parent_context_ids();
971 array_unshift($contexts, $context->id
);
973 foreach ($contexts as $ctxid) {
974 if ($records = $DB->get_records('grade_letters', array('contextid'=>$ctxid), 'lowerboundary DESC')) {
975 foreach ($records as $record) {
976 $letters[$record->lowerboundary
] = $record->letter
;
980 if (!empty($letters)) {
981 $cache[$context->id
] = $letters;
986 $letters = grade_get_letters(null);
987 $cache[$context->id
] = $letters;
993 * Verify new value of grade item idnumber. Checks for uniqueness of new ID numbers. Old ID numbers are kept intact.
995 * @param string $idnumber string (with magic quotes)
996 * @param int $courseid ID numbers are course unique only
997 * @param grade_item $grade_item The grade item this idnumber is associated with
998 * @param stdClass $cm used for course module idnumbers and items attached to modules
999 * @return bool true means idnumber ok
1001 function grade_verify_idnumber($idnumber, $courseid, $grade_item=null, $cm=null) {
1004 if ($idnumber == '') {
1005 //we allow empty idnumbers
1009 // keep existing even when not unique
1010 if ($cm and $cm->idnumber
== $idnumber) {
1011 if ($grade_item and $grade_item->itemnumber
!= 0) {
1012 // grade item with itemnumber > 0 can't have the same idnumber as the main
1013 // itemnumber 0 which is synced with course_modules
1017 } else if ($grade_item and $grade_item->idnumber
== $idnumber) {
1021 if ($DB->record_exists('course_modules', array('course'=>$courseid, 'idnumber'=>$idnumber))) {
1025 if ($DB->record_exists('grade_items', array('courseid'=>$courseid, 'idnumber'=>$idnumber))) {
1033 * Force final grade recalculation in all course items
1035 * @param int $courseid The course ID to recalculate
1037 function grade_force_full_regrading($courseid) {
1039 $DB->set_field('grade_items', 'needsupdate', 1, array('courseid'=>$courseid));
1043 * Forces regrading of all site grades. Used when changing site setings
1045 function grade_force_site_regrading() {
1047 $DB->set_field('grade_items', 'needsupdate', 1);
1051 * Recover a user's grades from grade_grades_history
1052 * @param int $userid the user ID whose grades we want to recover
1053 * @param int $courseid the relevant course
1054 * @return bool true if successful or false if there was an error or no grades could be recovered
1056 function grade_recover_history_grades($userid, $courseid) {
1059 if ($CFG->disablegradehistory
) {
1060 debugging('Attempting to recover grades when grade history is disabled.');
1064 //Were grades recovered? Flag to return.
1065 $recoveredgrades = false;
1067 //Check the user is enrolled in this course
1068 //Dont bother checking if they have a gradeable role. They may get one later so recover
1069 //whatever grades they have now just in case.
1070 $course_context = context_course
::instance($courseid);
1071 if (!is_enrolled($course_context, $userid)) {
1072 debugging('Attempting to recover the grades of a user who is deleted or not enrolled. Skipping recover.');
1076 //Check for existing grades for this user in this course
1077 //Recovering grades when the user already has grades can lead to duplicate indexes and bad data
1078 //In the future we could move the existing grades to the history table then recover the grades from before then
1079 $sql = "SELECT gg.id
1080 FROM {grade_grades} gg
1081 JOIN {grade_items} gi ON gi.id = gg.itemid
1082 WHERE gi.courseid = :courseid AND gg.userid = :userid";
1083 $params = array('userid' => $userid, 'courseid' => $courseid);
1084 if ($DB->record_exists_sql($sql, $params)) {
1085 debugging('Attempting to recover the grades of a user who already has grades. Skipping recover.');
1088 //Retrieve the user's old grades
1089 //have history ID as first column to guarantee we a unique first column
1090 $sql = "SELECT h.id, gi.itemtype, gi.itemmodule, gi.iteminstance as iteminstance, gi.itemnumber, h.source, h.itemid, h.userid, h.rawgrade, h.rawgrademax,
1091 h.rawgrademin, h.rawscaleid, h.usermodified, h.finalgrade, h.hidden, h.locked, h.locktime, h.exported, h.overridden, h.excluded, h.feedback,
1092 h.feedbackformat, h.information, h.informationformat, h.timemodified, itemcreated.tm AS timecreated
1093 FROM {grade_grades_history} h
1094 JOIN (SELECT itemid, MAX(id) AS id
1095 FROM {grade_grades_history}
1096 WHERE userid = :userid1
1097 GROUP BY itemid) maxquery ON h.id = maxquery.id AND h.itemid = maxquery.itemid
1098 JOIN {grade_items} gi ON gi.id = h.itemid
1099 JOIN (SELECT itemid, MAX(timemodified) AS tm
1100 FROM {grade_grades_history}
1101 WHERE userid = :userid2 AND action = :insertaction
1102 GROUP BY itemid) itemcreated ON itemcreated.itemid = h.itemid
1103 WHERE gi.courseid = :courseid";
1104 $params = array('userid1' => $userid, 'userid2' => $userid , 'insertaction' => GRADE_HISTORY_INSERT
, 'courseid' => $courseid);
1105 $oldgrades = $DB->get_records_sql($sql, $params);
1107 //now move the old grades to the grade_grades table
1108 foreach ($oldgrades as $oldgrade) {
1109 unset($oldgrade->id
);
1111 $grade = new grade_grade($oldgrade, false);//2nd arg false as dont want to try and retrieve a record from the DB
1112 $grade->insert($oldgrade->source
);
1114 //dont include default empty grades created when activities are created
1115 if (!is_null($oldgrade->finalgrade
) ||
!is_null($oldgrade->feedback
)) {
1116 $recoveredgrades = true;
1121 //Some activities require manual grade synching (moving grades from the activity into the gradebook)
1122 //If the student was deleted when synching was done they may have grades in the activity that haven't been moved across
1123 grade_grab_course_grades($courseid, null, $userid);
1125 return $recoveredgrades;
1129 * Updates all final grades in course.
1131 * @param int $courseid The course ID
1132 * @param int $userid If specified try to do a quick regrading of the grades of this user only
1133 * @param object $updated_item Optional grade item to be marked for regrading. It is required if $userid is set.
1134 * @param \core\progress\base $progress If provided, will be used to update progress on this long operation.
1135 * @return bool true if ok, array of errors if problems found. Grade item id => error message
1137 function grade_regrade_final_grades($courseid, $userid=null, $updated_item=null, $progress=null) {
1138 // This may take a very long time and extra memory.
1139 \core_php_time_limit
::raise();
1140 raise_memory_limit(MEMORY_EXTRA
);
1142 $course_item = grade_item
::fetch_course_item($courseid);
1144 if ($progress == null) {
1145 $progress = new \core\progress\none
();
1149 // one raw grade updated for one user
1150 if (empty($updated_item)) {
1151 print_error("cannotbenull", 'debug', '', "updated_item");
1153 if ($course_item->needsupdate
) {
1154 $updated_item->force_regrading();
1155 return array($course_item->id
=>'Can not do fast regrading after updating of raw grades');
1159 if (!$course_item->needsupdate
) {
1160 // nothing to do :-)
1165 // Categories might have to run some processing before we fetch the grade items.
1166 // This gives them a final opportunity to update and mark their children to be updated.
1167 // We need to work on the children categories up to the parent ones, so that, for instance,
1168 // if a category total is updated it will be reflected in the parent category.
1169 $cats = grade_category
::fetch_all(array('courseid' => $courseid));
1170 $flatcattree = array();
1171 foreach ($cats as $cat) {
1172 if (!isset($flatcattree[$cat->depth
])) {
1173 $flatcattree[$cat->depth
] = array();
1175 $flatcattree[$cat->depth
][] = $cat;
1177 krsort($flatcattree);
1178 foreach ($flatcattree as $depth => $cats) {
1179 foreach ($cats as $cat) {
1180 $cat->pre_regrade_final_grades();
1185 $progresscurrent = 0;
1187 $grade_items = grade_item
::fetch_all(array('courseid'=>$courseid));
1188 $depends_on = array();
1190 foreach ($grade_items as $gid=>$gitem) {
1191 if ((!empty($updated_item) and $updated_item->id
== $gid) ||
1192 $gitem->is_course_item() ||
$gitem->is_category_item() ||
$gitem->is_calculated()) {
1193 $grade_items[$gid]->needsupdate
= 1;
1196 // We load all dependencies of these items later we can discard some grade_items based on this.
1197 if ($grade_items[$gid]->needsupdate
) {
1198 $depends_on[$gid] = $grade_items[$gid]->depends_on();
1203 $progress->start_progress('regrade_course', $progresstotal);
1206 $finalids = array();
1207 $updatedids = array();
1208 $gids = array_keys($grade_items);
1211 while (count($finalids) < count($gids)) { // work until all grades are final or error found
1213 foreach ($gids as $gid) {
1214 if (in_array($gid, $finalids)) {
1215 continue; // already final
1218 if (!$grade_items[$gid]->needsupdate
) {
1219 $finalids[] = $gid; // we can make it final - does not need update
1222 $thisprogress = $progresstotal;
1223 foreach ($grade_items as $item) {
1224 if ($item->needsupdate
) {
1228 // Clip between $progresscurrent and $progresstotal.
1229 $thisprogress = max(min($thisprogress, $progresstotal), $progresscurrent);
1230 $progress->progress($thisprogress);
1231 $progresscurrent = $thisprogress;
1233 foreach ($depends_on[$gid] as $did) {
1234 if (!in_array($did, $finalids)) {
1235 // This item depends on something that is not yet in finals array.
1240 // If this grade item has no dependancy with any updated item at all, then remove it from being recalculated.
1242 // When we get here, all of this grade item's decendents are marked as final so they would be marked as updated too
1243 // if they would have been regraded. We don't need to regrade items which dependants (not only the direct ones
1244 // but any dependant in the cascade) have not been updated.
1246 // If $updated_item was specified we discard the grade items that do not depend on it or on any grade item that
1247 // depend on $updated_item.
1249 // Here we check to see if the direct decendants are marked as updated.
1250 if (!empty($updated_item) && $gid != $updated_item->id
&& !in_array($updated_item->id
, $depends_on[$gid])) {
1252 // We need to ensure that none of this item's dependencies have been updated.
1253 // If we find that one of the direct decendants of this grade item is marked as updated then this
1254 // grade item needs to be recalculated and marked as updated.
1255 // Being marked as updated is done further down in the code.
1257 $updateddependencies = false;
1258 foreach ($depends_on[$gid] as $dependency) {
1259 if (in_array($dependency, $updatedids)) {
1260 $updateddependencies = true;
1264 if ($updateddependencies === false) {
1265 // If no direct descendants are marked as updated, then we don't need to update this grade item. We then mark it
1273 // Let's update, calculate or aggregate.
1274 $result = $grade_items[$gid]->regrade_final_grades($userid);
1276 if ($result === true) {
1278 // We should only update the database if we regraded all users.
1279 if (empty($userid)) {
1280 $grade_items[$gid]->regrading_finished();
1281 // Do the locktime item locking.
1282 $grade_items[$gid]->check_locktime();
1284 $grade_items[$gid]->needsupdate
= 0;
1288 $updatedids[] = $gid;
1291 $grade_items[$gid]->force_regrading();
1292 $errors[$gid] = $result;
1303 foreach($gids as $gid) {
1304 if (in_array($gid, $finalids)) {
1305 continue; // this one is ok
1307 $grade_items[$gid]->force_regrading();
1308 $errors[$grade_items[$gid]->id
] = get_string('errorcalculationbroken', 'grades');
1310 break; // Found error.
1313 $progress->end_progress();
1315 if (count($errors) == 0) {
1316 if (empty($userid)) {
1317 // do the locktime locking of grades, but only when doing full regrading
1318 grade_grade
::check_locktime_all($gids);
1327 * Refetches grade data from course activities
1329 * @param int $courseid The course ID
1330 * @param string $modname Limit the grade fetch to a single module type. For example 'forum'
1331 * @param int $userid limit the grade fetch to a single user
1333 function grade_grab_course_grades($courseid, $modname=null, $userid=0) {
1337 $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname
1338 FROM {".$modname."} a, {course_modules} cm, {modules} m
1339 WHERE m.name=:modname AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=:courseid";
1340 $params = array('modname'=>$modname, 'courseid'=>$courseid);
1342 if ($modinstances = $DB->get_records_sql($sql, $params)) {
1343 foreach ($modinstances as $modinstance) {
1344 grade_update_mod_grades($modinstance, $userid);
1350 if (!$mods = core_component
::get_plugin_list('mod') ) {
1351 print_error('nomodules', 'debug');
1354 foreach ($mods as $mod => $fullmod) {
1355 if ($mod == 'NEWMODULE') { // Someone has unzipped the template, ignore it
1359 // include the module lib once
1360 if (file_exists($fullmod.'/lib.php')) {
1361 // get all instance of the activity
1362 $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname
1363 FROM {".$mod."} a, {course_modules} cm, {modules} m
1364 WHERE m.name=:mod AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=:courseid";
1365 $params = array('mod'=>$mod, 'courseid'=>$courseid);
1367 if ($modinstances = $DB->get_records_sql($sql, $params)) {
1368 foreach ($modinstances as $modinstance) {
1369 grade_update_mod_grades($modinstance, $userid);
1377 * Force full update of module grades in central gradebook
1379 * @param object $modinstance Module object with extra cmidnumber and modname property
1380 * @param int $userid Optional user ID if limiting the update to a single user
1381 * @return bool True if success
1383 function grade_update_mod_grades($modinstance, $userid=0) {
1386 $fullmod = $CFG->dirroot
.'/mod/'.$modinstance->modname
;
1387 if (!file_exists($fullmod.'/lib.php')) {
1388 debugging('missing lib.php file in module ' . $modinstance->modname
);
1391 include_once($fullmod.'/lib.php');
1393 $updateitemfunc = $modinstance->modname
.'_grade_item_update';
1394 $updategradesfunc = $modinstance->modname
.'_update_grades';
1396 if (function_exists($updategradesfunc) and function_exists($updateitemfunc)) {
1397 //new grading supported, force updating of grades
1398 $updateitemfunc($modinstance);
1399 $updategradesfunc($modinstance, $userid);
1400 } else if (function_exists($updategradesfunc) xor function_exists($updateitemfunc)) {
1401 // Module does not support grading?
1402 debugging("You have declared one of $updateitemfunc and $updategradesfunc but not both. " .
1403 "This will cause broken behaviour.", DEBUG_DEVELOPER
);
1410 * Remove grade letters for given context
1412 * @param context $context The context
1413 * @param bool $showfeedback If true a success notification will be displayed
1415 function remove_grade_letters($context, $showfeedback) {
1416 global $DB, $OUTPUT;
1418 $strdeleted = get_string('deleted');
1420 $records = $DB->get_records('grade_letters', array('contextid' => $context->id
));
1421 foreach ($records as $record) {
1422 $DB->delete_records('grade_letters', array('id' => $record->id
));
1423 // Trigger the letter grade deleted event.
1424 $event = \core\event\grade_letter_deleted
::create(array(
1425 'objectid' => $record->id
,
1426 'context' => $context,
1430 if ($showfeedback) {
1431 echo $OUTPUT->notification($strdeleted.' - '.get_string('letters', 'grades'), 'notifysuccess');
1436 * Remove all grade related course data
1437 * Grade history is kept
1439 * @param int $courseid The course ID
1440 * @param bool $showfeedback If true success notifications will be displayed
1442 function remove_course_grades($courseid, $showfeedback) {
1443 global $DB, $OUTPUT;
1445 $fs = get_file_storage();
1446 $strdeleted = get_string('deleted');
1448 $course_category = grade_category
::fetch_course_category($courseid);
1449 $course_category->delete('coursedelete');
1450 $fs->delete_area_files(context_course
::instance($courseid)->id
, 'grade', 'feedback');
1451 if ($showfeedback) {
1452 echo $OUTPUT->notification($strdeleted.' - '.get_string('grades', 'grades').', '.get_string('items', 'grades').', '.get_string('categories', 'grades'), 'notifysuccess');
1455 if ($outcomes = grade_outcome
::fetch_all(array('courseid'=>$courseid))) {
1456 foreach ($outcomes as $outcome) {
1457 $outcome->delete('coursedelete');
1460 $DB->delete_records('grade_outcomes_courses', array('courseid'=>$courseid));
1461 if ($showfeedback) {
1462 echo $OUTPUT->notification($strdeleted.' - '.get_string('outcomes', 'grades'), 'notifysuccess');
1465 if ($scales = grade_scale
::fetch_all(array('courseid'=>$courseid))) {
1466 foreach ($scales as $scale) {
1467 $scale->delete('coursedelete');
1470 if ($showfeedback) {
1471 echo $OUTPUT->notification($strdeleted.' - '.get_string('scales'), 'notifysuccess');
1474 $DB->delete_records('grade_settings', array('courseid'=>$courseid));
1475 if ($showfeedback) {
1476 echo $OUTPUT->notification($strdeleted.' - '.get_string('settings', 'grades'), 'notifysuccess');
1481 * Called when course category is deleted
1482 * Cleans the gradebook of associated data
1484 * @param int $categoryid The course category id
1485 * @param int $newparentid If empty everything is deleted. Otherwise the ID of the category where content moved
1486 * @param bool $showfeedback print feedback
1488 function grade_course_category_delete($categoryid, $newparentid, $showfeedback) {
1491 $context = context_coursecat
::instance($categoryid);
1492 $records = $DB->get_records('grade_letters', array('contextid' => $context->id
));
1493 foreach ($records as $record) {
1494 $DB->delete_records('grade_letters', array('id' => $record->id
));
1495 // Trigger the letter grade deleted event.
1496 $event = \core\event\grade_letter_deleted
::create(array(
1497 'objectid' => $record->id
,
1498 'context' => $context,
1505 * Does gradebook cleanup when a module is uninstalled
1506 * Deletes all associated grade items
1508 * @param string $modname The grade item module name to remove. For example 'forum'
1510 function grade_uninstalled_module($modname) {
1515 WHERE itemtype='mod' AND itemmodule=?";
1517 // go all items for this module and delete them including the grades
1518 $rs = $DB->get_recordset_sql($sql, array($modname));
1519 foreach ($rs as $item) {
1520 $grade_item = new grade_item($item, false);
1521 $grade_item->delete('moduninstall');
1527 * Deletes all of a user's grade data from gradebook
1529 * @param int $userid The user whose grade data should be deleted
1531 function grade_user_delete($userid) {
1532 if ($grades = grade_grade
::fetch_all(array('userid'=>$userid))) {
1533 foreach ($grades as $grade) {
1534 $grade->delete('userdelete');
1540 * Purge course data when user unenrolls from a course
1542 * @param int $courseid The ID of the course the user has unenrolled from
1543 * @param int $userid The ID of the user unenrolling
1545 function grade_user_unenrol($courseid, $userid) {
1546 if ($items = grade_item
::fetch_all(array('courseid'=>$courseid))) {
1547 foreach ($items as $item) {
1548 if ($grades = grade_grade
::fetch_all(array('userid'=>$userid, 'itemid'=>$item->id
))) {
1549 foreach ($grades as $grade) {
1550 $grade->delete('userdelete');
1558 * Reset all course grades, refetch from the activities and recalculate
1560 * @param int $courseid The course to reset
1561 * @return bool success
1563 function grade_course_reset($courseid) {
1565 // no recalculations
1566 grade_force_full_regrading($courseid);
1568 $grade_items = grade_item
::fetch_all(array('courseid'=>$courseid));
1569 foreach ($grade_items as $gid=>$grade_item) {
1570 $grade_item->delete_all_grades('reset');
1573 //refetch all grades
1574 grade_grab_course_grades($courseid);
1576 // recalculate all grades
1577 grade_regrade_final_grades($courseid);
1582 * Convert a number to 5 decimal point float, an empty string or a null db compatible format
1583 * (we need this to decide if db value changed)
1585 * @param mixed $number The number to convert
1586 * @return mixed float or null
1588 function grade_floatval($number) {
1589 if (is_null($number) or $number === '') {
1592 // we must round to 5 digits to get the same precision as in 10,5 db fields
1593 // note: db rounding for 10,5 is different from php round() function
1594 return round($number, 5);
1598 * Compare two float numbers safely. Uses 5 decimals php precision using {@link grade_floatval()}. Nulls accepted too.
1599 * Used for determining if a database update is required
1601 * @param float $f1 Float one to compare
1602 * @param float $f2 Float two to compare
1603 * @return bool True if the supplied values are different
1605 function grade_floats_different($f1, $f2) {
1606 // note: db rounding for 10,5 is different from php round() function
1607 return (grade_floatval($f1) !== grade_floatval($f2));
1611 * Compare two float numbers safely. Uses 5 decimals php precision using {@link grade_floatval()}
1613 * Do not use rounding for 10,5 at the database level as the results may be
1614 * different from php round() function.
1617 * @param float $f1 Float one to compare
1618 * @param float $f2 Float two to compare
1619 * @return bool True if the values should be considered as the same grades
1621 function grade_floats_equal($f1, $f2) {
1622 return (grade_floatval($f1) === grade_floatval($f2));
1626 * Get the most appropriate grade date for a grade item given the user that the grade relates to.
1628 * @param \stdClass $grade
1629 * @param \stdClass $user
1632 function grade_get_date_for_user_grade(\stdClass
$grade, \stdClass
$user): ?
int {
1633 // The `datesubmitted` is the time that the grade was created.
1634 // The `dategraded` is the time that it was modified or overwritten.
1635 // If the grade was last modified by the user themselves use the date graded.
1636 // Otherwise use date submitted.
1637 if ($grade->usermodified
== $user->id ||
empty($grade->datesubmitted
)) {
1638 return $grade->dategraded
;
1640 return $grade->datesubmitted
;