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();
27 /** Include essential files */
28 require_once($CFG->libdir
. '/grade/constants.php');
30 require_once($CFG->libdir
. '/grade/grade_category.php');
31 require_once($CFG->libdir
. '/grade/grade_item.php');
32 require_once($CFG->libdir
. '/grade/grade_grade.php');
33 require_once($CFG->libdir
. '/grade/grade_scale.php');
34 require_once($CFG->libdir
. '/grade/grade_outcome.php');
36 /////////////////////////////////////////////////////////////////////
37 ///// Start of public API for communication with modules/blocks /////
38 /////////////////////////////////////////////////////////////////////
41 * Submit new or update grade; update/create grade_item definition. Grade must have userid specified,
42 * rawgrade and feedback with format are optional. rawgrade NULL means 'Not graded'.
43 * Missing property or key means does not change the existing value.
45 * Only following grade item properties can be changed 'itemname', 'idnumber', 'gradetype', 'grademax',
46 * 'grademin', 'scaleid', 'multfactor', 'plusfactor', 'deleted' and 'hidden'. 'reset' means delete all current grades including locked ones.
48 * Manual, course or category items can not be updated by this function.
51 * @param string $source Source of the grade such as 'mod/assignment'
52 * @param int $courseid ID of course
53 * @param string $itemtype Type of grade item. For example, mod or block
54 * @param string $itemmodule More specific then $itemtype. For example, assignment or forum. May be NULL for some item types
55 * @param int $iteminstance Instance ID of graded item
56 * @param int $itemnumber Most probably 0. Modules can use other numbers when having more than one grade for each user
57 * @param mixed $grades Grade (object, array) or several grades (arrays of arrays or objects), NULL if updating grade_item definition only
58 * @param mixed $itemdetails Object or array describing the grading item, NULL if no change
59 * @return int Returns GRADE_UPDATE_OK, GRADE_UPDATE_FAILED, GRADE_UPDATE_MULTIPLE or GRADE_UPDATE_ITEM_LOCKED
61 function grade_update($source, $courseid, $itemtype, $itemmodule, $iteminstance, $itemnumber, $grades=NULL, $itemdetails=NULL) {
62 global $USER, $CFG, $DB;
64 // only following grade_item properties can be changed in this function
65 $allowed = array('itemname', 'idnumber', 'gradetype', 'grademax', 'grademin', 'scaleid', 'multfactor', 'plusfactor', 'deleted', 'hidden');
66 // list of 10,5 numeric fields
67 $floats = array('grademin', 'grademax', 'multfactor', 'plusfactor');
69 // grade item identification
70 $params = compact('courseid', 'itemtype', 'itemmodule', 'iteminstance', 'itemnumber');
72 if (is_null($courseid) or is_null($itemtype)) {
73 debugging('Missing courseid or itemtype');
74 return GRADE_UPDATE_FAILED
;
77 if (!$grade_items = grade_item
::fetch_all($params)) {
81 } else if (count($grade_items) == 1){
82 $grade_item = reset($grade_items);
83 unset($grade_items); //release memory
86 debugging('Found more than one grade item');
87 return GRADE_UPDATE_MULTIPLE
;
90 if (!empty($itemdetails['deleted'])) {
92 if ($grade_item->delete($source)) {
93 return GRADE_UPDATE_OK
;
95 return GRADE_UPDATE_FAILED
;
98 return GRADE_UPDATE_OK
;
101 /// Create or update the grade_item if needed
105 $itemdetails = (array)$itemdetails;
107 // grademin and grademax ignored when scale specified
108 if (array_key_exists('scaleid', $itemdetails)) {
109 if ($itemdetails['scaleid']) {
110 unset($itemdetails['grademin']);
111 unset($itemdetails['grademax']);
115 foreach ($itemdetails as $k=>$v) {
116 if (!in_array($k, $allowed)) {
120 if ($k == 'gradetype' and $v == GRADE_TYPE_NONE
) {
121 // no grade item needed!
122 return GRADE_UPDATE_OK
;
127 $grade_item = new grade_item($params);
128 $grade_item->insert();
131 if ($grade_item->is_locked()) {
132 // no notice() here, test returned value instead!
133 return GRADE_UPDATE_ITEM_LOCKED
;
137 $itemdetails = (array)$itemdetails;
139 foreach ($itemdetails as $k=>$v) {
140 if (!in_array($k, $allowed)) {
144 if (in_array($k, $floats)) {
145 if (grade_floats_different($grade_item->{$k}, $v)) {
146 $grade_item->{$k} = $v;
151 if ($grade_item->{$k} != $v) {
152 $grade_item->{$k} = $v;
158 $grade_item->update();
163 /// reset grades if requested
164 if (!empty($itemdetails['reset'])) {
165 $grade_item->delete_all_grades('reset');
166 return GRADE_UPDATE_OK
;
169 /// Some extra checks
170 // do we use grading?
171 if ($grade_item->gradetype
== GRADE_TYPE_NONE
) {
172 return GRADE_UPDATE_OK
;
175 // no grade submitted
176 if (empty($grades)) {
177 return GRADE_UPDATE_OK
;
180 /// Finally start processing of grades
181 if (is_object($grades)) {
182 $grades = array($grades->userid
=>$grades);
184 if (array_key_exists('userid', $grades)) {
185 $grades = array($grades['userid']=>$grades);
189 /// normalize and verify grade array
190 foreach($grades as $k=>$g) {
196 if (empty($g['userid']) or $k != $g['userid']) {
197 debugging('Incorrect grade array index, must be user id! Grade ignored.');
202 if (empty($grades)) {
203 return GRADE_UPDATE_FAILED
;
206 $count = count($grades);
207 if ($count > 0 and $count < 200) {
208 list($uids, $params) = $DB->get_in_or_equal(array_keys($grades), SQL_PARAMS_NAMED
, $start='uid');
209 $params['gid'] = $grade_item->id
;
210 $sql = "SELECT * FROM {grade_grades} WHERE itemid = :gid AND userid $uids";
213 $sql = "SELECT * FROM {grade_grades} WHERE itemid = :gid";
214 $params = array('gid'=>$grade_item->id
);
217 $rs = $DB->get_recordset_sql($sql, $params);
221 while (count($grades) > 0) {
225 foreach ($rs as $gd) {
227 $userid = $gd->userid
;
228 if (!isset($grades[$userid])) {
229 // this grade not requested, continue
232 // existing grade requested
233 $grade = $grades[$userid];
234 $grade_grade = new grade_grade($gd, false);
235 unset($grades[$userid]);
239 if (is_null($grade_grade)) {
240 if (count($grades) == 0) {
241 // no more grades to process
245 $grade = reset($grades);
246 $userid = $grade['userid'];
247 $grade_grade = new grade_grade(array('itemid'=>$grade_item->id
, 'userid'=>$userid), false);
248 $grade_grade->load_optional_fields(); // add feedback and info too
249 unset($grades[$userid]);
254 $feedbackformat = FORMAT_MOODLE
;
255 $usermodified = $USER->id
;
256 $datesubmitted = null;
259 if (array_key_exists('rawgrade', $grade)) {
260 $rawgrade = $grade['rawgrade'];
263 if (array_key_exists('feedback', $grade)) {
264 $feedback = $grade['feedback'];
267 if (array_key_exists('feedbackformat', $grade)) {
268 $feedbackformat = $grade['feedbackformat'];
271 if (array_key_exists('usermodified', $grade)) {
272 $usermodified = $grade['usermodified'];
275 if (array_key_exists('datesubmitted', $grade)) {
276 $datesubmitted = $grade['datesubmitted'];
279 if (array_key_exists('dategraded', $grade)) {
280 $dategraded = $grade['dategraded'];
283 // update or insert the grade
284 if (!$grade_item->update_raw_grade($userid, $rawgrade, $source, $feedback, $feedbackformat, $usermodified, $dategraded, $datesubmitted, $grade_grade)) {
294 return GRADE_UPDATE_OK
;
296 return GRADE_UPDATE_FAILED
;
301 * Updates a user's outcomes. Manual outcomes can not be updated.
304 * @param string $source Source of the grade such as 'mod/assignment'
305 * @param int $courseid ID of course
306 * @param string $itemtype Type of grade item. For example, 'mod' or 'block'
307 * @param string $itemmodule More specific then $itemtype. For example, 'forum' or 'quiz'. May be NULL for some item types
308 * @param int $iteminstance Instance ID of graded item. For example the forum ID.
309 * @param int $userid ID of the graded user
310 * @param array $data Array consisting of grade item itemnumber ({@link grade_update()}) => outcomegrade
311 * @return bool returns true if grade items were found and updated successfully
313 function grade_update_outcomes($source, $courseid, $itemtype, $itemmodule, $iteminstance, $userid, $data) {
314 if ($items = grade_item
::fetch_all(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid))) {
316 foreach ($items as $item) {
317 if (!array_key_exists($item->itemnumber
, $data)) {
320 $grade = $data[$item->itemnumber
] < 1 ?
null : $data[$item->itemnumber
];
321 $result = ($item->update_final_grade($userid, $grade, $source) && $result);
325 return false; //grade items not found
329 * Return true if the course needs regrading.
331 * @param int $courseid The course ID
332 * @return bool true if course grades need updating.
334 function grade_needs_regrade_final_grades($courseid) {
335 $course_item = grade_item
::fetch_course_item($courseid);
336 return $course_item->needsupdate
;
340 * Return true if the regrade process is likely to be time consuming and
341 * will therefore require the progress bar.
343 * @param int $courseid The course ID
344 * @return bool Whether the regrade process is likely to be time consuming
346 function grade_needs_regrade_progress_bar($courseid) {
348 $grade_items = grade_item
::fetch_all(array('courseid' => $courseid));
350 list($sql, $params) = $DB->get_in_or_equal(array_keys($grade_items), SQL_PARAMS_NAMED
, 'gi');
351 $gradecount = $DB->count_records_select('grade_grades', 'itemid ' . $sql, $params);
353 // This figure may seem arbitrary, but after analysis it seems that 100 grade_grades can be calculated in ~= 0.5 seconds.
354 // Any longer than this and we want to show the progress bar.
355 return $gradecount > 100;
359 * Check whether regarding of final grades is required and, if so, perform the regrade.
361 * If the regrade is expected to be time consuming (see grade_needs_regrade_progress_bar), then this
362 * function will output the progress bar, and redirect to the current PAGE->url after regrading
363 * completes. Otherwise the regrading will happen immediately and the page will be loaded as per
366 * A callback may be specified, which is called if regrading has taken place.
367 * The callback may optionally return a URL which will be redirected to when the progress bar is present.
369 * @param stdClass $course The course to regrade
370 * @param callable $callback A function to call if regrading took place
371 * @return moodle_url The URL to redirect to if redirecting
373 function grade_regrade_final_grades_if_required($course, callable
$callback = null) {
374 global $PAGE, $OUTPUT;
376 if (!grade_needs_regrade_final_grades($course->id
)) {
380 if (grade_needs_regrade_progress_bar($course->id
)) {
381 $PAGE->set_heading($course->fullname
);
382 echo $OUTPUT->header();
383 echo $OUTPUT->heading(get_string('recalculatinggrades', 'grades'));
384 $progress = new \core\progress\
display(true);
385 $status = grade_regrade_final_grades($course->id
, null, null, $progress);
387 // Show regrade errors and set the course to no longer needing regrade (stop endless loop).
388 if (is_array($status)) {
389 foreach ($status as $error) {
390 $errortext = new \core\output\notification
($error, \core\output\notification
::NOTIFY_ERROR
);
391 echo $OUTPUT->render($errortext);
393 $courseitem = grade_item
::fetch_course_item($course->id
);
394 $courseitem->regrading_finished();
399 $url = call_user_func($callback);
406 echo $OUTPUT->continue_button($url);
407 echo $OUTPUT->footer();
410 $result = grade_regrade_final_grades($course->id
);
412 call_user_func($callback);
419 * Returns grading information for given activity, optionally with user grades
420 * Manual, course or category items can not be queried.
423 * @param int $courseid ID of course
424 * @param string $itemtype Type of grade item. For example, 'mod' or 'block'
425 * @param string $itemmodule More specific then $itemtype. For example, 'forum' or 'quiz'. May be NULL for some item types
426 * @param int $iteminstance ID of the item module
427 * @param mixed $userid_or_ids Either a single user ID, an array of user IDs or null. If user ID or IDs are not supplied returns information about grade_item
428 * @return array Array of grade information objects (scaleid, name, grade and locked status, etc.) indexed with itemnumbers
430 function grade_get_grades($courseid, $itemtype, $itemmodule, $iteminstance, $userid_or_ids=null) {
433 $return = new stdClass();
434 $return->items
= array();
435 $return->outcomes
= array();
437 $course_item = grade_item
::fetch_course_item($courseid);
438 $needsupdate = array();
439 if ($course_item->needsupdate
) {
440 $result = grade_regrade_final_grades($courseid);
441 if ($result !== true) {
442 $needsupdate = array_keys($result);
446 if ($grade_items = grade_item
::fetch_all(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid))) {
447 foreach ($grade_items as $grade_item) {
448 $decimalpoints = null;
450 if (empty($grade_item->outcomeid
)) {
451 // prepare information about grade item
452 $item = new stdClass();
453 $item->id
= $grade_item->id
;
454 $item->itemnumber
= $grade_item->itemnumber
;
455 $item->itemtype
= $grade_item->itemtype
;
456 $item->itemmodule
= $grade_item->itemmodule
;
457 $item->iteminstance
= $grade_item->iteminstance
;
458 $item->scaleid
= $grade_item->scaleid
;
459 $item->name
= $grade_item->get_name();
460 $item->grademin
= $grade_item->grademin
;
461 $item->grademax
= $grade_item->grademax
;
462 $item->gradepass
= $grade_item->gradepass
;
463 $item->locked
= $grade_item->is_locked();
464 $item->hidden
= $grade_item->is_hidden();
465 $item->grades
= array();
467 switch ($grade_item->gradetype
) {
468 case GRADE_TYPE_NONE
:
471 case GRADE_TYPE_VALUE
:
475 case GRADE_TYPE_TEXT
:
479 $item->gradepass
= 0;
483 if (empty($userid_or_ids)) {
486 } else if (is_array($userid_or_ids)) {
487 $userids = $userid_or_ids;
490 $userids = array($userid_or_ids);
494 $grade_grades = grade_grade
::fetch_users_grades($grade_item, $userids, true);
495 foreach ($userids as $userid) {
496 $grade_grades[$userid]->grade_item
=& $grade_item;
498 $grade = new stdClass();
499 $grade->grade
= $grade_grades[$userid]->finalgrade
;
500 $grade->locked
= $grade_grades[$userid]->is_locked();
501 $grade->hidden
= $grade_grades[$userid]->is_hidden();
502 $grade->overridden
= $grade_grades[$userid]->overridden
;
503 $grade->feedback
= $grade_grades[$userid]->feedback
;
504 $grade->feedbackformat
= $grade_grades[$userid]->feedbackformat
;
505 $grade->usermodified
= $grade_grades[$userid]->usermodified
;
506 $grade->datesubmitted
= $grade_grades[$userid]->get_datesubmitted();
507 $grade->dategraded
= $grade_grades[$userid]->get_dategraded();
509 // create text representation of grade
510 if ($grade_item->gradetype
== GRADE_TYPE_TEXT
or $grade_item->gradetype
== GRADE_TYPE_NONE
) {
511 $grade->grade
= null;
512 $grade->str_grade
= '-';
513 $grade->str_long_grade
= $grade->str_grade
;
515 } else if (in_array($grade_item->id
, $needsupdate)) {
516 $grade->grade
= false;
517 $grade->str_grade
= get_string('error');
518 $grade->str_long_grade
= $grade->str_grade
;
520 } else if (is_null($grade->grade
)) {
521 $grade->str_grade
= '-';
522 $grade->str_long_grade
= $grade->str_grade
;
525 $grade->str_grade
= grade_format_gradevalue($grade->grade
, $grade_item);
526 if ($grade_item->gradetype
== GRADE_TYPE_SCALE
or $grade_item->get_displaytype() != GRADE_DISPLAY_TYPE_REAL
) {
527 $grade->str_long_grade
= $grade->str_grade
;
530 $a->grade
= $grade->str_grade
;
531 $a->max
= grade_format_gradevalue($grade_item->grademax
, $grade_item);
532 $grade->str_long_grade
= get_string('gradelong', 'grades', $a);
536 // create html representation of feedback
537 if (is_null($grade->feedback
)) {
538 $grade->str_feedback
= '';
540 $grade->str_feedback
= format_text($grade->feedback
, $grade->feedbackformat
);
543 $item->grades
[$userid] = $grade;
546 $return->items
[$grade_item->itemnumber
] = $item;
549 if (!$grade_outcome = grade_outcome
::fetch(array('id'=>$grade_item->outcomeid
))) {
550 debugging('Incorect outcomeid found');
555 $outcome = new stdClass();
556 $outcome->id
= $grade_item->id
;
557 $outcome->itemnumber
= $grade_item->itemnumber
;
558 $outcome->itemtype
= $grade_item->itemtype
;
559 $outcome->itemmodule
= $grade_item->itemmodule
;
560 $outcome->iteminstance
= $grade_item->iteminstance
;
561 $outcome->scaleid
= $grade_outcome->scaleid
;
562 $outcome->name
= $grade_outcome->get_name();
563 $outcome->locked
= $grade_item->is_locked();
564 $outcome->hidden
= $grade_item->is_hidden();
566 if (empty($userid_or_ids)) {
568 } else if (is_array($userid_or_ids)) {
569 $userids = $userid_or_ids;
571 $userids = array($userid_or_ids);
575 $grade_grades = grade_grade
::fetch_users_grades($grade_item, $userids, true);
576 foreach ($userids as $userid) {
577 $grade_grades[$userid]->grade_item
=& $grade_item;
579 $grade = new stdClass();
580 $grade->grade
= $grade_grades[$userid]->finalgrade
;
581 $grade->locked
= $grade_grades[$userid]->is_locked();
582 $grade->hidden
= $grade_grades[$userid]->is_hidden();
583 $grade->feedback
= $grade_grades[$userid]->feedback
;
584 $grade->feedbackformat
= $grade_grades[$userid]->feedbackformat
;
585 $grade->usermodified
= $grade_grades[$userid]->usermodified
;
587 // create text representation of grade
588 if (in_array($grade_item->id
, $needsupdate)) {
589 $grade->grade
= false;
590 $grade->str_grade
= get_string('error');
592 } else if (is_null($grade->grade
)) {
594 $grade->str_grade
= get_string('nooutcome', 'grades');
597 $grade->grade
= (int)$grade->grade
;
598 $scale = $grade_item->load_scale();
599 $grade->str_grade
= format_string($scale->scale_items
[(int)$grade->grade
-1]);
602 // create html representation of feedback
603 if (is_null($grade->feedback
)) {
604 $grade->str_feedback
= '';
606 $grade->str_feedback
= format_text($grade->feedback
, $grade->feedbackformat
);
609 $outcome->grades
[$userid] = $grade;
613 if (isset($return->outcomes
[$grade_item->itemnumber
])) {
614 // itemnumber duplicates - lets fix them!
615 $newnumber = $grade_item->itemnumber +
1;
616 while(grade_item
::fetch(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid, 'itemnumber'=>$newnumber))) {
619 $outcome->itemnumber
= $newnumber;
620 $grade_item->itemnumber
= $newnumber;
621 $grade_item->update('system');
624 $return->outcomes
[$grade_item->itemnumber
] = $outcome;
630 // sort results using itemnumbers
631 ksort($return->items
, SORT_NUMERIC
);
632 ksort($return->outcomes
, SORT_NUMERIC
);
637 ///////////////////////////////////////////////////////////////////
638 ///// End of public API for communication with modules/blocks /////
639 ///////////////////////////////////////////////////////////////////
643 ///////////////////////////////////////////////////////////////////
644 ///// Internal API: used by gradebook plugins and Moodle core /////
645 ///////////////////////////////////////////////////////////////////
648 * Returns a course gradebook setting
650 * @param int $courseid
651 * @param string $name of setting, maybe null if reset only
652 * @param string $default value to return if setting is not found
653 * @param bool $resetcache force reset of internal static cache
654 * @return string value of the setting, $default if setting not found, NULL if supplied $name is null
656 function grade_get_setting($courseid, $name, $default=null, $resetcache=false) {
659 static $cache = array();
661 if ($resetcache or !array_key_exists($courseid, $cache)) {
662 $cache[$courseid] = array();
664 } else if (is_null($name)) {
667 } else if (array_key_exists($name, $cache[$courseid])) {
668 return $cache[$courseid][$name];
671 if (!$data = $DB->get_record('grade_settings', array('courseid'=>$courseid, 'name'=>$name))) {
674 $result = $data->value
;
677 if (is_null($result)) {
681 $cache[$courseid][$name] = $result;
686 * Returns all course gradebook settings as object properties
688 * @param int $courseid
691 function grade_get_settings($courseid) {
694 $settings = new stdClass();
695 $settings->id
= $courseid;
697 if ($records = $DB->get_records('grade_settings', array('courseid'=>$courseid))) {
698 foreach ($records as $record) {
699 $settings->{$record->name
} = $record->value
;
707 * Add, update or delete a course gradebook setting
709 * @param int $courseid The course ID
710 * @param string $name Name of the setting
711 * @param string $value Value of the setting. NULL means delete the setting.
713 function grade_set_setting($courseid, $name, $value) {
716 if (is_null($value)) {
717 $DB->delete_records('grade_settings', array('courseid'=>$courseid, 'name'=>$name));
719 } else if (!$existing = $DB->get_record('grade_settings', array('courseid'=>$courseid, 'name'=>$name))) {
720 $data = new stdClass();
721 $data->courseid
= $courseid;
723 $data->value
= $value;
724 $DB->insert_record('grade_settings', $data);
727 $data = new stdClass();
728 $data->id
= $existing->id
;
729 $data->value
= $value;
730 $DB->update_record('grade_settings', $data);
733 grade_get_setting($courseid, null, null, true); // reset the cache
737 * Returns string representation of grade value
739 * @param float $value The grade value
740 * @param object $grade_item Grade item object passed by reference to prevent scale reloading
741 * @param bool $localized use localised decimal separator
742 * @param int $displaytype type of display. For example GRADE_DISPLAY_TYPE_REAL, GRADE_DISPLAY_TYPE_PERCENTAGE, GRADE_DISPLAY_TYPE_LETTER
743 * @param int $decimals The number of decimal places when displaying float values
746 function grade_format_gradevalue($value, &$grade_item, $localized=true, $displaytype=null, $decimals=null) {
747 if ($grade_item->gradetype
== GRADE_TYPE_NONE
or $grade_item->gradetype
== GRADE_TYPE_TEXT
) {
752 if (is_null($value)) {
756 if ($grade_item->gradetype
!= GRADE_TYPE_VALUE
and $grade_item->gradetype
!= GRADE_TYPE_SCALE
) {
761 if (is_null($displaytype)) {
762 $displaytype = $grade_item->get_displaytype();
765 if (is_null($decimals)) {
766 $decimals = $grade_item->get_decimals();
769 switch ($displaytype) {
770 case GRADE_DISPLAY_TYPE_REAL
:
771 return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized);
773 case GRADE_DISPLAY_TYPE_PERCENTAGE
:
774 return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized);
776 case GRADE_DISPLAY_TYPE_LETTER
:
777 return grade_format_gradevalue_letter($value, $grade_item);
779 case GRADE_DISPLAY_TYPE_REAL_PERCENTAGE
:
780 return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ' (' .
781 grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ')';
783 case GRADE_DISPLAY_TYPE_REAL_LETTER
:
784 return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ' (' .
785 grade_format_gradevalue_letter($value, $grade_item) . ')';
787 case GRADE_DISPLAY_TYPE_PERCENTAGE_REAL
:
788 return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ' (' .
789 grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ')';
791 case GRADE_DISPLAY_TYPE_LETTER_REAL
:
792 return grade_format_gradevalue_letter($value, $grade_item) . ' (' .
793 grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ')';
795 case GRADE_DISPLAY_TYPE_LETTER_PERCENTAGE
:
796 return grade_format_gradevalue_letter($value, $grade_item) . ' (' .
797 grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ')';
799 case GRADE_DISPLAY_TYPE_PERCENTAGE_LETTER
:
800 return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ' (' .
801 grade_format_gradevalue_letter($value, $grade_item) . ')';
808 * Returns a float representation of a grade value
810 * @param float $value The grade value
811 * @param object $grade_item Grade item object
812 * @param int $decimals The number of decimal places
813 * @param bool $localized use localised decimal separator
816 function grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) {
817 if ($grade_item->gradetype
== GRADE_TYPE_SCALE
) {
818 if (!$scale = $grade_item->load_scale()) {
819 return get_string('error');
822 $value = $grade_item->bounded_grade($value);
823 return format_string($scale->scale_items
[$value-1]);
826 return format_float($value, $decimals, $localized);
831 * Returns a percentage representation of a grade value
833 * @param float $value The grade value
834 * @param object $grade_item Grade item object
835 * @param int $decimals The number of decimal places
836 * @param bool $localized use localised decimal separator
839 function grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) {
840 $min = $grade_item->grademin
;
841 $max = $grade_item->grademax
;
845 $value = $grade_item->bounded_grade($value);
846 $percentage = (($value-$min)*100)/($max-$min);
847 return format_float($percentage, $decimals, $localized).' %';
851 * Returns a letter grade representation of a grade value
852 * The array of grade letters used is produced by {@link grade_get_letters()} using the course context
854 * @param float $value The grade value
855 * @param object $grade_item Grade item object
858 function grade_format_gradevalue_letter($value, $grade_item) {
860 $context = context_course
::instance($grade_item->courseid
, IGNORE_MISSING
);
861 if (!$letters = grade_get_letters($context)) {
862 return ''; // no letters??
865 if (is_null($value)) {
869 $value = grade_grade
::standardise_score($value, $grade_item->grademin
, $grade_item->grademax
, 0, 100);
870 $value = bounded_number(0, $value, 100); // just in case
872 $gradebookcalculationsfreeze = 'gradebook_calculations_freeze_' . $grade_item->courseid
;
874 foreach ($letters as $boundary => $letter) {
875 if (property_exists($CFG, $gradebookcalculationsfreeze) && (int)$CFG->{$gradebookcalculationsfreeze} <= 20160518) {
878 // The boundary is a percentage out of 100 so use 0 as the min and 100 as the max.
879 $boundary = grade_grade
::standardise_score($boundary, 0, 100, 0, 100);
881 if ($value >= $boundary) {
882 return format_string($letter);
885 return '-'; // no match? maybe '' would be more correct
890 * Returns grade options for gradebook grade category menu
892 * @param int $courseid The course ID
893 * @param bool $includenew Include option for new category at array index -1
894 * @return array of grade categories in course
896 function grade_get_categories_menu($courseid, $includenew=false) {
898 if (!$categories = grade_category
::fetch_all(array('courseid'=>$courseid))) {
899 //make sure course category exists
900 if (!grade_category
::fetch_course_category($courseid)) {
901 debugging('Can not create course grade category!');
904 $categories = grade_category
::fetch_all(array('courseid'=>$courseid));
906 foreach ($categories as $key=>$category) {
907 if ($category->is_course_category()) {
908 $result[$category->id
] = get_string('uncategorised', 'grades');
909 unset($categories[$key]);
913 $result[-1] = get_string('newcategory', 'grades');
916 foreach ($categories as $category) {
917 $cats[$category->id
] = $category->get_name();
919 core_collator
::asort($cats);
921 return ($result+
$cats);
925 * Returns the array of grade letters to be used in the supplied context
927 * @param object $context Context object or null for defaults
928 * @return array of grade_boundary (minimum) => letter_string
930 function grade_get_letters($context=null) {
933 if (empty($context)) {
934 //default grading letters
935 return array('93'=>'A', '90'=>'A-', '87'=>'B+', '83'=>'B', '80'=>'B-', '77'=>'C+', '73'=>'C', '70'=>'C-', '67'=>'D+', '60'=>'D', '0'=>'F');
938 static $cache = array();
940 if (array_key_exists($context->id
, $cache)) {
941 return $cache[$context->id
];
944 if (count($cache) > 100) {
945 $cache = array(); // cache size limit
950 $contexts = $context->get_parent_context_ids();
951 array_unshift($contexts, $context->id
);
953 foreach ($contexts as $ctxid) {
954 if ($records = $DB->get_records('grade_letters', array('contextid'=>$ctxid), 'lowerboundary DESC')) {
955 foreach ($records as $record) {
956 $letters[$record->lowerboundary
] = $record->letter
;
960 if (!empty($letters)) {
961 $cache[$context->id
] = $letters;
966 $letters = grade_get_letters(null);
967 $cache[$context->id
] = $letters;
973 * Verify new value of grade item idnumber. Checks for uniqueness of new ID numbers. Old ID numbers are kept intact.
975 * @param string $idnumber string (with magic quotes)
976 * @param int $courseid ID numbers are course unique only
977 * @param grade_item $grade_item The grade item this idnumber is associated with
978 * @param stdClass $cm used for course module idnumbers and items attached to modules
979 * @return bool true means idnumber ok
981 function grade_verify_idnumber($idnumber, $courseid, $grade_item=null, $cm=null) {
984 if ($idnumber == '') {
985 //we allow empty idnumbers
989 // keep existing even when not unique
990 if ($cm and $cm->idnumber
== $idnumber) {
991 if ($grade_item and $grade_item->itemnumber
!= 0) {
992 // grade item with itemnumber > 0 can't have the same idnumber as the main
993 // itemnumber 0 which is synced with course_modules
997 } else if ($grade_item and $grade_item->idnumber
== $idnumber) {
1001 if ($DB->record_exists('course_modules', array('course'=>$courseid, 'idnumber'=>$idnumber))) {
1005 if ($DB->record_exists('grade_items', array('courseid'=>$courseid, 'idnumber'=>$idnumber))) {
1013 * Force final grade recalculation in all course items
1015 * @param int $courseid The course ID to recalculate
1017 function grade_force_full_regrading($courseid) {
1019 $DB->set_field('grade_items', 'needsupdate', 1, array('courseid'=>$courseid));
1023 * Forces regrading of all site grades. Used when changing site setings
1025 function grade_force_site_regrading() {
1027 $DB->set_field('grade_items', 'needsupdate', 1);
1031 * Recover a user's grades from grade_grades_history
1032 * @param int $userid the user ID whose grades we want to recover
1033 * @param int $courseid the relevant course
1034 * @return bool true if successful or false if there was an error or no grades could be recovered
1036 function grade_recover_history_grades($userid, $courseid) {
1039 if ($CFG->disablegradehistory
) {
1040 debugging('Attempting to recover grades when grade history is disabled.');
1044 //Were grades recovered? Flag to return.
1045 $recoveredgrades = false;
1047 //Check the user is enrolled in this course
1048 //Dont bother checking if they have a gradeable role. They may get one later so recover
1049 //whatever grades they have now just in case.
1050 $course_context = context_course
::instance($courseid);
1051 if (!is_enrolled($course_context, $userid)) {
1052 debugging('Attempting to recover the grades of a user who is deleted or not enrolled. Skipping recover.');
1056 //Check for existing grades for this user in this course
1057 //Recovering grades when the user already has grades can lead to duplicate indexes and bad data
1058 //In the future we could move the existing grades to the history table then recover the grades from before then
1059 $sql = "SELECT gg.id
1060 FROM {grade_grades} gg
1061 JOIN {grade_items} gi ON gi.id = gg.itemid
1062 WHERE gi.courseid = :courseid AND gg.userid = :userid";
1063 $params = array('userid' => $userid, 'courseid' => $courseid);
1064 if ($DB->record_exists_sql($sql, $params)) {
1065 debugging('Attempting to recover the grades of a user who already has grades. Skipping recover.');
1068 //Retrieve the user's old grades
1069 //have history ID as first column to guarantee we a unique first column
1070 $sql = "SELECT h.id, gi.itemtype, gi.itemmodule, gi.iteminstance as iteminstance, gi.itemnumber, h.source, h.itemid, h.userid, h.rawgrade, h.rawgrademax,
1071 h.rawgrademin, h.rawscaleid, h.usermodified, h.finalgrade, h.hidden, h.locked, h.locktime, h.exported, h.overridden, h.excluded, h.feedback,
1072 h.feedbackformat, h.information, h.informationformat, h.timemodified, itemcreated.tm AS timecreated
1073 FROM {grade_grades_history} h
1074 JOIN (SELECT itemid, MAX(id) AS id
1075 FROM {grade_grades_history}
1076 WHERE userid = :userid1
1077 GROUP BY itemid) maxquery ON h.id = maxquery.id AND h.itemid = maxquery.itemid
1078 JOIN {grade_items} gi ON gi.id = h.itemid
1079 JOIN (SELECT itemid, MAX(timemodified) AS tm
1080 FROM {grade_grades_history}
1081 WHERE userid = :userid2 AND action = :insertaction
1082 GROUP BY itemid) itemcreated ON itemcreated.itemid = h.itemid
1083 WHERE gi.courseid = :courseid";
1084 $params = array('userid1' => $userid, 'userid2' => $userid , 'insertaction' => GRADE_HISTORY_INSERT
, 'courseid' => $courseid);
1085 $oldgrades = $DB->get_records_sql($sql, $params);
1087 //now move the old grades to the grade_grades table
1088 foreach ($oldgrades as $oldgrade) {
1089 unset($oldgrade->id
);
1091 $grade = new grade_grade($oldgrade, false);//2nd arg false as dont want to try and retrieve a record from the DB
1092 $grade->insert($oldgrade->source
);
1094 //dont include default empty grades created when activities are created
1095 if (!is_null($oldgrade->finalgrade
) ||
!is_null($oldgrade->feedback
)) {
1096 $recoveredgrades = true;
1101 //Some activities require manual grade synching (moving grades from the activity into the gradebook)
1102 //If the student was deleted when synching was done they may have grades in the activity that haven't been moved across
1103 grade_grab_course_grades($courseid, null, $userid);
1105 return $recoveredgrades;
1109 * Updates all final grades in course.
1111 * @param int $courseid The course ID
1112 * @param int $userid If specified try to do a quick regrading of the grades of this user only
1113 * @param object $updated_item Optional grade item to be marked for regrading
1114 * @param \core\progress\base $progress If provided, will be used to update progress on this long operation.
1115 * @return bool true if ok, array of errors if problems found. Grade item id => error message
1117 function grade_regrade_final_grades($courseid, $userid=null, $updated_item=null, $progress=null) {
1118 // This may take a very long time and extra memory.
1119 \core_php_time_limit
::raise();
1120 raise_memory_limit(MEMORY_EXTRA
);
1122 $course_item = grade_item
::fetch_course_item($courseid);
1124 if ($progress == null) {
1125 $progress = new \core\progress\none
();
1129 // one raw grade updated for one user
1130 if (empty($updated_item)) {
1131 print_error("cannotbenull", 'debug', '', "updated_item");
1133 if ($course_item->needsupdate
) {
1134 $updated_item->force_regrading();
1135 return array($course_item->id
=>'Can not do fast regrading after updating of raw grades');
1139 if (!$course_item->needsupdate
) {
1140 // nothing to do :-)
1145 // Categories might have to run some processing before we fetch the grade items.
1146 // This gives them a final opportunity to update and mark their children to be updated.
1147 // We need to work on the children categories up to the parent ones, so that, for instance,
1148 // if a category total is updated it will be reflected in the parent category.
1149 $cats = grade_category
::fetch_all(array('courseid' => $courseid));
1150 $flatcattree = array();
1151 foreach ($cats as $cat) {
1152 if (!isset($flatcattree[$cat->depth
])) {
1153 $flatcattree[$cat->depth
] = array();
1155 $flatcattree[$cat->depth
][] = $cat;
1157 krsort($flatcattree);
1158 foreach ($flatcattree as $depth => $cats) {
1159 foreach ($cats as $cat) {
1160 $cat->pre_regrade_final_grades();
1165 $progresscurrent = 0;
1167 $grade_items = grade_item
::fetch_all(array('courseid'=>$courseid));
1168 $depends_on = array();
1170 foreach ($grade_items as $gid=>$gitem) {
1171 if ((!empty($updated_item) and $updated_item->id
== $gid) ||
1172 $gitem->is_course_item() ||
$gitem->is_category_item() ||
$gitem->is_calculated()) {
1173 $grade_items[$gid]->needsupdate
= 1;
1176 // We load all dependencies of these items later we can discard some grade_items based on this.
1177 if ($grade_items[$gid]->needsupdate
) {
1178 $depends_on[$gid] = $grade_items[$gid]->depends_on();
1183 $progress->start_progress('regrade_course', $progresstotal);
1186 $finalids = array();
1187 $updatedids = array();
1188 $gids = array_keys($grade_items);
1191 while (count($finalids) < count($gids)) { // work until all grades are final or error found
1193 foreach ($gids as $gid) {
1194 if (in_array($gid, $finalids)) {
1195 continue; // already final
1198 if (!$grade_items[$gid]->needsupdate
) {
1199 $finalids[] = $gid; // we can make it final - does not need update
1202 $thisprogress = $progresstotal;
1203 foreach ($grade_items as $item) {
1204 if ($item->needsupdate
) {
1208 // Clip between $progresscurrent and $progresstotal.
1209 $thisprogress = max(min($thisprogress, $progresstotal), $progresscurrent);
1210 $progress->progress($thisprogress);
1211 $progresscurrent = $thisprogress;
1213 foreach ($depends_on[$gid] as $did) {
1214 if (!in_array($did, $finalids)) {
1215 // This item depends on something that is not yet in finals array.
1220 // If this grade item has no dependancy with any updated item at all, then remove it from being recalculated.
1222 // When we get here, all of this grade item's decendents are marked as final so they would be marked as updated too
1223 // if they would have been regraded. We don't need to regrade items which dependants (not only the direct ones
1224 // but any dependant in the cascade) have not been updated.
1226 // If $updated_item was specified we discard the grade items that do not depend on it or on any grade item that
1227 // depend on $updated_item.
1229 // Here we check to see if the direct decendants are marked as updated.
1230 if (!empty($updated_item) && $gid != $updated_item->id
&& !in_array($updated_item->id
, $depends_on[$gid])) {
1232 // We need to ensure that none of this item's dependencies have been updated.
1233 // If we find that one of the direct decendants of this grade item is marked as updated then this
1234 // grade item needs to be recalculated and marked as updated.
1235 // Being marked as updated is done further down in the code.
1237 $updateddependencies = false;
1238 foreach ($depends_on[$gid] as $dependency) {
1239 if (in_array($dependency, $updatedids)) {
1240 $updateddependencies = true;
1244 if ($updateddependencies === false) {
1245 // If no direct descendants are marked as updated, then we don't need to update this grade item. We then mark it
1253 // Let's update, calculate or aggregate.
1254 $result = $grade_items[$gid]->regrade_final_grades($userid);
1256 if ($result === true) {
1258 // We should only update the database if we regraded all users.
1259 if (empty($userid)) {
1260 $grade_items[$gid]->regrading_finished();
1261 // Do the locktime item locking.
1262 $grade_items[$gid]->check_locktime();
1264 $grade_items[$gid]->needsupdate
= 0;
1268 $updatedids[] = $gid;
1271 $grade_items[$gid]->force_regrading();
1272 $errors[$gid] = $result;
1283 foreach($gids as $gid) {
1284 if (in_array($gid, $finalids)) {
1285 continue; // this one is ok
1287 $grade_items[$gid]->force_regrading();
1288 $errors[$grade_items[$gid]->id
] = get_string('errorcalculationbroken', 'grades');
1290 break; // Found error.
1293 $progress->end_progress();
1295 if (count($errors) == 0) {
1296 if (empty($userid)) {
1297 // do the locktime locking of grades, but only when doing full regrading
1298 grade_grade
::check_locktime_all($gids);
1307 * Refetches grade data from course activities
1309 * @param int $courseid The course ID
1310 * @param string $modname Limit the grade fetch to a single module type. For example 'forum'
1311 * @param int $userid limit the grade fetch to a single user
1313 function grade_grab_course_grades($courseid, $modname=null, $userid=0) {
1317 $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname
1318 FROM {".$modname."} a, {course_modules} cm, {modules} m
1319 WHERE m.name=:modname AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=:courseid";
1320 $params = array('modname'=>$modname, 'courseid'=>$courseid);
1322 if ($modinstances = $DB->get_records_sql($sql, $params)) {
1323 foreach ($modinstances as $modinstance) {
1324 grade_update_mod_grades($modinstance, $userid);
1330 if (!$mods = core_component
::get_plugin_list('mod') ) {
1331 print_error('nomodules', 'debug');
1334 foreach ($mods as $mod => $fullmod) {
1335 if ($mod == 'NEWMODULE') { // Someone has unzipped the template, ignore it
1339 // include the module lib once
1340 if (file_exists($fullmod.'/lib.php')) {
1341 // get all instance of the activity
1342 $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname
1343 FROM {".$mod."} a, {course_modules} cm, {modules} m
1344 WHERE m.name=:mod AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=:courseid";
1345 $params = array('mod'=>$mod, 'courseid'=>$courseid);
1347 if ($modinstances = $DB->get_records_sql($sql, $params)) {
1348 foreach ($modinstances as $modinstance) {
1349 grade_update_mod_grades($modinstance, $userid);
1357 * Force full update of module grades in central gradebook
1359 * @param object $modinstance Module object with extra cmidnumber and modname property
1360 * @param int $userid Optional user ID if limiting the update to a single user
1361 * @return bool True if success
1363 function grade_update_mod_grades($modinstance, $userid=0) {
1366 $fullmod = $CFG->dirroot
.'/mod/'.$modinstance->modname
;
1367 if (!file_exists($fullmod.'/lib.php')) {
1368 debugging('missing lib.php file in module ' . $modinstance->modname
);
1371 include_once($fullmod.'/lib.php');
1373 $updateitemfunc = $modinstance->modname
.'_grade_item_update';
1374 $updategradesfunc = $modinstance->modname
.'_update_grades';
1376 if (function_exists($updategradesfunc) and function_exists($updateitemfunc)) {
1377 //new grading supported, force updating of grades
1378 $updateitemfunc($modinstance);
1379 $updategradesfunc($modinstance, $userid);
1380 } else if (function_exists($updategradesfunc) xor function_exists($updateitemfunc)) {
1381 // Module does not support grading?
1382 debugging("You have declared one of $updateitemfunc and $updategradesfunc but not both. " .
1383 "This will cause broken behaviour.", DEBUG_DEVELOPER
);
1390 * Remove grade letters for given context
1392 * @param context $context The context
1393 * @param bool $showfeedback If true a success notification will be displayed
1395 function remove_grade_letters($context, $showfeedback) {
1396 global $DB, $OUTPUT;
1398 $strdeleted = get_string('deleted');
1400 $DB->delete_records('grade_letters', array('contextid'=>$context->id
));
1401 if ($showfeedback) {
1402 echo $OUTPUT->notification($strdeleted.' - '.get_string('letters', 'grades'), 'notifysuccess');
1407 * Remove all grade related course data
1408 * Grade history is kept
1410 * @param int $courseid The course ID
1411 * @param bool $showfeedback If true success notifications will be displayed
1413 function remove_course_grades($courseid, $showfeedback) {
1414 global $DB, $OUTPUT;
1416 $fs = get_file_storage();
1417 $strdeleted = get_string('deleted');
1419 $course_category = grade_category
::fetch_course_category($courseid);
1420 $course_category->delete('coursedelete');
1421 $fs->delete_area_files(context_course
::instance($courseid)->id
, 'grade', 'feedback');
1422 if ($showfeedback) {
1423 echo $OUTPUT->notification($strdeleted.' - '.get_string('grades', 'grades').', '.get_string('items', 'grades').', '.get_string('categories', 'grades'), 'notifysuccess');
1426 if ($outcomes = grade_outcome
::fetch_all(array('courseid'=>$courseid))) {
1427 foreach ($outcomes as $outcome) {
1428 $outcome->delete('coursedelete');
1431 $DB->delete_records('grade_outcomes_courses', array('courseid'=>$courseid));
1432 if ($showfeedback) {
1433 echo $OUTPUT->notification($strdeleted.' - '.get_string('outcomes', 'grades'), 'notifysuccess');
1436 if ($scales = grade_scale
::fetch_all(array('courseid'=>$courseid))) {
1437 foreach ($scales as $scale) {
1438 $scale->delete('coursedelete');
1441 if ($showfeedback) {
1442 echo $OUTPUT->notification($strdeleted.' - '.get_string('scales'), 'notifysuccess');
1445 $DB->delete_records('grade_settings', array('courseid'=>$courseid));
1446 if ($showfeedback) {
1447 echo $OUTPUT->notification($strdeleted.' - '.get_string('settings', 'grades'), 'notifysuccess');
1452 * Called when course category is deleted
1453 * Cleans the gradebook of associated data
1455 * @param int $categoryid The course category id
1456 * @param int $newparentid If empty everything is deleted. Otherwise the ID of the category where content moved
1457 * @param bool $showfeedback print feedback
1459 function grade_course_category_delete($categoryid, $newparentid, $showfeedback) {
1462 $context = context_coursecat
::instance($categoryid);
1463 $DB->delete_records('grade_letters', array('contextid'=>$context->id
));
1467 * Does gradebook cleanup when a module is uninstalled
1468 * Deletes all associated grade items
1470 * @param string $modname The grade item module name to remove. For example 'forum'
1472 function grade_uninstalled_module($modname) {
1477 WHERE itemtype='mod' AND itemmodule=?";
1479 // go all items for this module and delete them including the grades
1480 $rs = $DB->get_recordset_sql($sql, array($modname));
1481 foreach ($rs as $item) {
1482 $grade_item = new grade_item($item, false);
1483 $grade_item->delete('moduninstall');
1489 * Deletes all of a user's grade data from gradebook
1491 * @param int $userid The user whose grade data should be deleted
1493 function grade_user_delete($userid) {
1494 if ($grades = grade_grade
::fetch_all(array('userid'=>$userid))) {
1495 foreach ($grades as $grade) {
1496 $grade->delete('userdelete');
1502 * Purge course data when user unenrolls from a course
1504 * @param int $courseid The ID of the course the user has unenrolled from
1505 * @param int $userid The ID of the user unenrolling
1507 function grade_user_unenrol($courseid, $userid) {
1508 if ($items = grade_item
::fetch_all(array('courseid'=>$courseid))) {
1509 foreach ($items as $item) {
1510 if ($grades = grade_grade
::fetch_all(array('userid'=>$userid, 'itemid'=>$item->id
))) {
1511 foreach ($grades as $grade) {
1512 $grade->delete('userdelete');
1520 * Grading cron job. Performs background clean up on the gradebook
1522 function grade_cron() {
1528 FROM {grade_items} i
1529 WHERE i.locked = 0 AND i.locktime > 0 AND i.locktime < ? AND EXISTS (
1530 SELECT 'x' FROM {grade_items} c WHERE c.itemtype='course' AND c.needsupdate=0 AND c.courseid=i.courseid)";
1532 // go through all courses that have proper final grades and lock them if needed
1533 $rs = $DB->get_recordset_sql($sql, array($now));
1534 foreach ($rs as $item) {
1535 $grade_item = new grade_item($item, false);
1536 $grade_item->locked
= $now;
1537 $grade_item->update('locktime');
1541 $grade_inst = new grade_grade();
1542 $fields = 'g.'.implode(',g.', $grade_inst->required_fields
);
1544 $sql = "SELECT $fields
1545 FROM {grade_grades} g, {grade_items} i
1546 WHERE g.locked = 0 AND g.locktime > 0 AND g.locktime < ? AND g.itemid=i.id AND EXISTS (
1547 SELECT 'x' FROM {grade_items} c WHERE c.itemtype='course' AND c.needsupdate=0 AND c.courseid=i.courseid)";
1549 // go through all courses that have proper final grades and lock them if needed
1550 $rs = $DB->get_recordset_sql($sql, array($now));
1551 foreach ($rs as $grade) {
1552 $grade_grade = new grade_grade($grade, false);
1553 $grade_grade->locked
= $now;
1554 $grade_grade->update('locktime');
1558 //TODO: do not run this cleanup every cron invocation
1559 // cleanup history tables
1560 if (!empty($CFG->gradehistorylifetime
)) { // value in days
1561 $histlifetime = $now - ($CFG->gradehistorylifetime
* 3600 * 24);
1562 $tables = array('grade_outcomes_history', 'grade_categories_history', 'grade_items_history', 'grade_grades_history', 'scale_history');
1563 foreach ($tables as $table) {
1564 if ($DB->delete_records_select($table, "timemodified < ?", array($histlifetime))) {
1565 mtrace(" Deleted old grade history records from '$table'");
1572 * Reset all course grades, refetch from the activities and recalculate
1574 * @param int $courseid The course to reset
1575 * @return bool success
1577 function grade_course_reset($courseid) {
1579 // no recalculations
1580 grade_force_full_regrading($courseid);
1582 $grade_items = grade_item
::fetch_all(array('courseid'=>$courseid));
1583 foreach ($grade_items as $gid=>$grade_item) {
1584 $grade_item->delete_all_grades('reset');
1587 //refetch all grades
1588 grade_grab_course_grades($courseid);
1590 // recalculate all grades
1591 grade_regrade_final_grades($courseid);
1596 * Convert a number to 5 decimal point float, an empty string or a null db compatible format
1597 * (we need this to decide if db value changed)
1599 * @param mixed $number The number to convert
1600 * @return mixed float or null
1602 function grade_floatval($number) {
1603 if (is_null($number) or $number === '') {
1606 // we must round to 5 digits to get the same precision as in 10,5 db fields
1607 // note: db rounding for 10,5 is different from php round() function
1608 return round($number, 5);
1612 * Compare two float numbers safely. Uses 5 decimals php precision using {@link grade_floatval()}. Nulls accepted too.
1613 * Used for determining if a database update is required
1615 * @param float $f1 Float one to compare
1616 * @param float $f2 Float two to compare
1617 * @return bool True if the supplied values are different
1619 function grade_floats_different($f1, $f2) {
1620 // note: db rounding for 10,5 is different from php round() function
1621 return (grade_floatval($f1) !== grade_floatval($f2));
1625 * Compare two float numbers safely. Uses 5 decimals php precision using {@link grade_floatval()}
1627 * Do not use rounding for 10,5 at the database level as the results may be
1628 * different from php round() function.
1631 * @param float $f1 Float one to compare
1632 * @param float $f2 Float two to compare
1633 * @return bool True if the values should be considered as the same grades
1635 function grade_floats_equal($f1, $f2) {
1636 return (grade_floatval($f1) === grade_floatval($f2));