NOMDL fixed typo in variable name
[moodle.git] / lib / gradelib.php
blobc0dad2075d8523de1c42cb3d438db9d5dbec4586
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
18 /**
19 * Library of functions for gradebook - both public and internal
21 * @package core
22 * @subpackage grade
23 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27 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 /////////////////////////////////////////////////////////////////////
42 /**
43 * Submit new or update grade; update/create grade_item definition. Grade must have userid specified,
44 * rawgrade and feedback with format are optional. rawgrade NULL means 'Not graded', missing property
45 * or key means do not change existing.
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.
51 * @access public
52 * @global object
53 * @global object
54 * @global object
55 * @param string $source source of the grade such as 'mod/assignment'
56 * @param int $courseid id of course
57 * @param string $itemtype type of grade item - mod, block
58 * @param string $itemmodule more specific then $itemtype - assignment, forum, etc.; maybe NULL for some item types
59 * @param int $iteminstance instance it of graded subject
60 * @param int $itemnumber most probably 0, modules can use other numbers when having more than one grades for each user
61 * @param mixed $grades grade (object, array) or several grades (arrays of arrays or objects), NULL if updating grade_item definition only
62 * @param mixed $itemdetails object or array describing the grading item, NULL if no change
64 function grade_update($source, $courseid, $itemtype, $itemmodule, $iteminstance, $itemnumber, $grades=NULL, $itemdetails=NULL) {
65 global $USER, $CFG, $DB;
67 // only following grade_item properties can be changed in this function
68 $allowed = array('itemname', 'idnumber', 'gradetype', 'grademax', 'grademin', 'scaleid', 'multfactor', 'plusfactor', 'deleted', 'hidden');
69 // list of 10,5 numeric fields
70 $floats = array('grademin', 'grademax', 'multfactor', 'plusfactor');
72 // grade item identification
73 $params = compact('courseid', 'itemtype', 'itemmodule', 'iteminstance', 'itemnumber');
75 if (is_null($courseid) or is_null($itemtype)) {
76 debugging('Missing courseid or itemtype');
77 return GRADE_UPDATE_FAILED;
80 if (!$grade_items = grade_item::fetch_all($params)) {
81 // create a new one
82 $grade_item = false;
84 } else if (count($grade_items) == 1){
85 $grade_item = reset($grade_items);
86 unset($grade_items); //release memory
88 } else {
89 debugging('Found more than one grade item');
90 return GRADE_UPDATE_MULTIPLE;
93 if (!empty($itemdetails['deleted'])) {
94 if ($grade_item) {
95 if ($grade_item->delete($source)) {
96 return GRADE_UPDATE_OK;
97 } else {
98 return GRADE_UPDATE_FAILED;
101 return GRADE_UPDATE_OK;
104 /// Create or update the grade_item if needed
106 if (!$grade_item) {
107 if ($itemdetails) {
108 $itemdetails = (array)$itemdetails;
110 // grademin and grademax ignored when scale specified
111 if (array_key_exists('scaleid', $itemdetails)) {
112 if ($itemdetails['scaleid']) {
113 unset($itemdetails['grademin']);
114 unset($itemdetails['grademax']);
118 foreach ($itemdetails as $k=>$v) {
119 if (!in_array($k, $allowed)) {
120 // ignore it
121 continue;
123 if ($k == 'gradetype' and $v == GRADE_TYPE_NONE) {
124 // no grade item needed!
125 return GRADE_UPDATE_OK;
127 $params[$k] = $v;
130 $grade_item = new grade_item($params);
131 $grade_item->insert();
133 } else {
134 if ($grade_item->is_locked()) {
135 // no notice() here, test returned value instead!
136 return GRADE_UPDATE_ITEM_LOCKED;
139 if ($itemdetails) {
140 $itemdetails = (array)$itemdetails;
141 $update = false;
142 foreach ($itemdetails as $k=>$v) {
143 if (!in_array($k, $allowed)) {
144 // ignore it
145 continue;
147 if (in_array($k, $floats)) {
148 if (grade_floats_different($grade_item->{$k}, $v)) {
149 $grade_item->{$k} = $v;
150 $update = true;
153 } else {
154 if ($grade_item->{$k} != $v) {
155 $grade_item->{$k} = $v;
156 $update = true;
160 if ($update) {
161 $grade_item->update();
166 /// reset grades if requested
167 if (!empty($itemdetails['reset'])) {
168 $grade_item->delete_all_grades('reset');
169 return GRADE_UPDATE_OK;
172 /// Some extra checks
173 // do we use grading?
174 if ($grade_item->gradetype == GRADE_TYPE_NONE) {
175 return GRADE_UPDATE_OK;
178 // no grade submitted
179 if (empty($grades)) {
180 return GRADE_UPDATE_OK;
183 /// Finally start processing of grades
184 if (is_object($grades)) {
185 $grades = array($grades->userid=>$grades);
186 } else {
187 if (array_key_exists('userid', $grades)) {
188 $grades = array($grades['userid']=>$grades);
192 /// normalize and verify grade array
193 foreach($grades as $k=>$g) {
194 if (!is_array($g)) {
195 $g = (array)$g;
196 $grades[$k] = $g;
199 if (empty($g['userid']) or $k != $g['userid']) {
200 debugging('Incorrect grade array index, must be user id! Grade ignored.');
201 unset($grades[$k]);
205 if (empty($grades)) {
206 return GRADE_UPDATE_FAILED;
209 $count = count($grades);
210 if ($count > 0 and $count < 200) {
211 list($uids, $params) = $DB->get_in_or_equal(array_keys($grades), SQL_PARAMS_NAMED, $start='uid0');
212 $params['gid'] = $grade_item->id;
213 $sql = "SELECT * FROM {grade_grades} WHERE itemid = :gid AND userid $uids";
215 } else {
216 $sql = "SELECT * FROM {grade_grades} WHERE itemid = :gid";
217 $params = array('gid'=>$grade_item->id);
220 $rs = $DB->get_recordset_sql($sql, $params);
222 $failed = false;
224 while (count($grades) > 0) {
225 $grade_grade = null;
226 $grade = null;
228 foreach ($rs as $gd) {
230 $userid = $gd->userid;
231 if (!isset($grades[$userid])) {
232 // this grade not requested, continue
233 continue;
235 // existing grade requested
236 $grade = $grades[$userid];
237 $grade_grade = new grade_grade($gd, false);
238 unset($grades[$userid]);
239 break;
242 if (is_null($grade_grade)) {
243 if (count($grades) == 0) {
244 // no more grades to process
245 break;
248 $grade = reset($grades);
249 $userid = $grade['userid'];
250 $grade_grade = new grade_grade(array('itemid'=>$grade_item->id, 'userid'=>$userid), false);
251 $grade_grade->load_optional_fields(); // add feedback and info too
252 unset($grades[$userid]);
255 $rawgrade = false;
256 $feedback = false;
257 $feedbackformat = FORMAT_MOODLE;
258 $usermodified = $USER->id;
259 $datesubmitted = null;
260 $dategraded = null;
262 if (array_key_exists('rawgrade', $grade)) {
263 $rawgrade = $grade['rawgrade'];
266 if (array_key_exists('feedback', $grade)) {
267 $feedback = $grade['feedback'];
270 if (array_key_exists('feedbackformat', $grade)) {
271 $feedbackformat = $grade['feedbackformat'];
274 if (array_key_exists('usermodified', $grade)) {
275 $usermodified = $grade['usermodified'];
278 if (array_key_exists('datesubmitted', $grade)) {
279 $datesubmitted = $grade['datesubmitted'];
282 if (array_key_exists('dategraded', $grade)) {
283 $dategraded = $grade['dategraded'];
286 // update or insert the grade
287 if (!$grade_item->update_raw_grade($userid, $rawgrade, $source, $feedback, $feedbackformat, $usermodified, $dategraded, $datesubmitted, $grade_grade)) {
288 $failed = true;
292 if ($rs) {
293 $rs->close();
296 if (!$failed) {
297 return GRADE_UPDATE_OK;
298 } else {
299 return GRADE_UPDATE_FAILED;
304 * Updates outcomes of user
305 * Manual outcomes can not be updated.
307 * @access public
308 * @param string $source source of the grade such as 'mod/assignment'
309 * @param int $courseid id of course
310 * @param string $itemtype 'mod', 'block'
311 * @param string $itemmodule 'forum, 'quiz', etc.
312 * @param int $iteminstance id of the item module
313 * @param int $userid ID of the graded user
314 * @param array $data array itemnumber=>outcomegrade
315 * @return boolean returns true if grade items were found and updated successfully
317 function grade_update_outcomes($source, $courseid, $itemtype, $itemmodule, $iteminstance, $userid, $data) {
318 if ($items = grade_item::fetch_all(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid))) {
319 $result = true;
320 foreach ($items as $item) {
321 if (!array_key_exists($item->itemnumber, $data)) {
322 continue;
324 $grade = $data[$item->itemnumber] < 1 ? null : $data[$item->itemnumber];
325 $result = ($item->update_final_grade($userid, $grade, $source) && $result);
327 return $result;
329 return false; //grade items not found
333 * Returns grading information for given activity - optionally with users grades
334 * Manual, course or category items can not be queried.
336 * @access public
337 * @global object
338 * @param int $courseid id of course
339 * @param string $itemtype 'mod', 'block'
340 * @param string $itemmodule 'forum, 'quiz', etc.
341 * @param int $iteminstance id of the item module
342 * @param array|int $userid_or_ids optional id of the graded user or array of ids; if userid not used, returns only information about grade_item
343 * @return array Array of grade information objects (scaleid, name, grade and locked status, etc.) indexed with itemnumbers
345 function grade_get_grades($courseid, $itemtype, $itemmodule, $iteminstance, $userid_or_ids=null) {
346 global $CFG;
348 $return = new stdClass();
349 $return->items = array();
350 $return->outcomes = array();
352 $course_item = grade_item::fetch_course_item($courseid);
353 $needsupdate = array();
354 if ($course_item->needsupdate) {
355 $result = grade_regrade_final_grades($courseid);
356 if ($result !== true) {
357 $needsupdate = array_keys($result);
361 if ($grade_items = grade_item::fetch_all(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid))) {
362 foreach ($grade_items as $grade_item) {
363 $decimalpoints = null;
365 if (empty($grade_item->outcomeid)) {
366 // prepare information about grade item
367 $item = new stdClass();
368 $item->itemnumber = $grade_item->itemnumber;
369 $item->scaleid = $grade_item->scaleid;
370 $item->name = $grade_item->get_name();
371 $item->grademin = $grade_item->grademin;
372 $item->grademax = $grade_item->grademax;
373 $item->gradepass = $grade_item->gradepass;
374 $item->locked = $grade_item->is_locked();
375 $item->hidden = $grade_item->is_hidden();
376 $item->grades = array();
378 switch ($grade_item->gradetype) {
379 case GRADE_TYPE_NONE:
380 continue;
382 case GRADE_TYPE_VALUE:
383 $item->scaleid = 0;
384 break;
386 case GRADE_TYPE_TEXT:
387 $item->scaleid = 0;
388 $item->grademin = 0;
389 $item->grademax = 0;
390 $item->gradepass = 0;
391 break;
394 if (empty($userid_or_ids)) {
395 $userids = array();
397 } else if (is_array($userid_or_ids)) {
398 $userids = $userid_or_ids;
400 } else {
401 $userids = array($userid_or_ids);
404 if ($userids) {
405 $grade_grades = grade_grade::fetch_users_grades($grade_item, $userids, true);
406 foreach ($userids as $userid) {
407 $grade_grades[$userid]->grade_item =& $grade_item;
409 $grade = new stdClass();
410 $grade->grade = $grade_grades[$userid]->finalgrade;
411 $grade->locked = $grade_grades[$userid]->is_locked();
412 $grade->hidden = $grade_grades[$userid]->is_hidden();
413 $grade->overridden = $grade_grades[$userid]->overridden;
414 $grade->feedback = $grade_grades[$userid]->feedback;
415 $grade->feedbackformat = $grade_grades[$userid]->feedbackformat;
416 $grade->usermodified = $grade_grades[$userid]->usermodified;
417 $grade->datesubmitted = $grade_grades[$userid]->get_datesubmitted();
418 $grade->dategraded = $grade_grades[$userid]->get_dategraded();
420 // create text representation of grade
421 if ($grade_item->gradetype == GRADE_TYPE_TEXT or $grade_item->gradetype == GRADE_TYPE_NONE) {
422 $grade->grade = null;
423 $grade->str_grade = '-';
424 $grade->str_long_grade = $grade->str_grade;
426 } else if (in_array($grade_item->id, $needsupdate)) {
427 $grade->grade = false;
428 $grade->str_grade = get_string('error');
429 $grade->str_long_grade = $grade->str_grade;
431 } else if (is_null($grade->grade)) {
432 $grade->str_grade = '-';
433 $grade->str_long_grade = $grade->str_grade;
435 } else {
436 $grade->str_grade = grade_format_gradevalue($grade->grade, $grade_item);
437 if ($grade_item->gradetype == GRADE_TYPE_SCALE or $grade_item->get_displaytype() != GRADE_DISPLAY_TYPE_REAL) {
438 $grade->str_long_grade = $grade->str_grade;
439 } else {
440 $a = new stdClass();
441 $a->grade = $grade->str_grade;
442 $a->max = grade_format_gradevalue($grade_item->grademax, $grade_item);
443 $grade->str_long_grade = get_string('gradelong', 'grades', $a);
447 // create html representation of feedback
448 if (is_null($grade->feedback)) {
449 $grade->str_feedback = '';
450 } else {
451 $grade->str_feedback = format_text($grade->feedback, $grade->feedbackformat);
454 $item->grades[$userid] = $grade;
457 $return->items[$grade_item->itemnumber] = $item;
459 } else {
460 if (!$grade_outcome = grade_outcome::fetch(array('id'=>$grade_item->outcomeid))) {
461 debugging('Incorect outcomeid found');
462 continue;
465 // outcome info
466 $outcome = new stdClass();
467 $outcome->itemnumber = $grade_item->itemnumber;
468 $outcome->scaleid = $grade_outcome->scaleid;
469 $outcome->name = $grade_outcome->get_name();
470 $outcome->locked = $grade_item->is_locked();
471 $outcome->hidden = $grade_item->is_hidden();
473 if (empty($userid_or_ids)) {
474 $userids = array();
475 } else if (is_array($userid_or_ids)) {
476 $userids = $userid_or_ids;
477 } else {
478 $userids = array($userid_or_ids);
481 if ($userids) {
482 $grade_grades = grade_grade::fetch_users_grades($grade_item, $userids, true);
483 foreach ($userids as $userid) {
484 $grade_grades[$userid]->grade_item =& $grade_item;
486 $grade = new stdClass();
487 $grade->grade = $grade_grades[$userid]->finalgrade;
488 $grade->locked = $grade_grades[$userid]->is_locked();
489 $grade->hidden = $grade_grades[$userid]->is_hidden();
490 $grade->feedback = $grade_grades[$userid]->feedback;
491 $grade->feedbackformat = $grade_grades[$userid]->feedbackformat;
492 $grade->usermodified = $grade_grades[$userid]->usermodified;
494 // create text representation of grade
495 if (in_array($grade_item->id, $needsupdate)) {
496 $grade->grade = false;
497 $grade->str_grade = get_string('error');
499 } else if (is_null($grade->grade)) {
500 $grade->grade = 0;
501 $grade->str_grade = get_string('nooutcome', 'grades');
503 } else {
504 $grade->grade = (int)$grade->grade;
505 $scale = $grade_item->load_scale();
506 $grade->str_grade = format_string($scale->scale_items[(int)$grade->grade-1]);
509 // create html representation of feedback
510 if (is_null($grade->feedback)) {
511 $grade->str_feedback = '';
512 } else {
513 $grade->str_feedback = format_text($grade->feedback, $grade->feedbackformat);
516 $outcome->grades[$userid] = $grade;
520 if (isset($return->outcomes[$grade_item->itemnumber])) {
521 // itemnumber duplicates - lets fix them!
522 $newnumber = $grade_item->itemnumber + 1;
523 while(grade_item::fetch(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid, 'itemnumber'=>$newnumber))) {
524 $newnumber++;
526 $outcome->itemnumber = $newnumber;
527 $grade_item->itemnumber = $newnumber;
528 $grade_item->update('system');
531 $return->outcomes[$grade_item->itemnumber] = $outcome;
537 // sort results using itemnumbers
538 ksort($return->items, SORT_NUMERIC);
539 ksort($return->outcomes, SORT_NUMERIC);
541 return $return;
544 ///////////////////////////////////////////////////////////////////
545 ///// End of public API for communication with modules/blocks /////
546 ///////////////////////////////////////////////////////////////////
550 ///////////////////////////////////////////////////////////////////
551 ///// Internal API: used by gradebook plugins and Moodle core /////
552 ///////////////////////////////////////////////////////////////////
555 * Returns course gradebook setting
557 * @global object
558 * @param int $courseid
559 * @param string $name of setting, maybe null if reset only
560 * @param string $default
561 * @param bool $resetcache force reset of internal static cache
562 * @return string value, NULL if no setting
564 function grade_get_setting($courseid, $name, $default=null, $resetcache=false) {
565 global $DB;
567 static $cache = array();
569 if ($resetcache or !array_key_exists($courseid, $cache)) {
570 $cache[$courseid] = array();
572 } else if (is_null($name)) {
573 return null;
575 } else if (array_key_exists($name, $cache[$courseid])) {
576 return $cache[$courseid][$name];
579 if (!$data = $DB->get_record('grade_settings', array('courseid'=>$courseid, 'name'=>$name))) {
580 $result = null;
581 } else {
582 $result = $data->value;
585 if (is_null($result)) {
586 $result = $default;
589 $cache[$courseid][$name] = $result;
590 return $result;
594 * Returns all course gradebook settings as object properties
596 * @global object
597 * @param int $courseid
598 * @return object
600 function grade_get_settings($courseid) {
601 global $DB;
603 $settings = new stdClass();
604 $settings->id = $courseid;
606 if ($records = $DB->get_records('grade_settings', array('courseid'=>$courseid))) {
607 foreach ($records as $record) {
608 $settings->{$record->name} = $record->value;
612 return $settings;
616 * Add/update course gradebook setting
618 * @global object
619 * @param int $courseid
620 * @param string $name of setting
621 * @param string value, NULL means no setting==remove
622 * @return void
624 function grade_set_setting($courseid, $name, $value) {
625 global $DB;
627 if (is_null($value)) {
628 $DB->delete_records('grade_settings', array('courseid'=>$courseid, 'name'=>$name));
630 } else if (!$existing = $DB->get_record('grade_settings', array('courseid'=>$courseid, 'name'=>$name))) {
631 $data = new stdClass();
632 $data->courseid = $courseid;
633 $data->name = $name;
634 $data->value = $value;
635 $DB->insert_record('grade_settings', $data);
637 } else {
638 $data = new stdClass();
639 $data->id = $existing->id;
640 $data->value = $value;
641 $DB->update_record('grade_settings', $data);
644 grade_get_setting($courseid, null, null, true); // reset the cache
648 * Returns string representation of grade value
650 * @param float $value grade value
651 * @param object $grade_item - by reference to prevent scale reloading
652 * @param bool $localized use localised decimal separator
653 * @param int $displaytype type of display - GRADE_DISPLAY_TYPE_REAL, GRADE_DISPLAY_TYPE_PERCENTAGE, GRADE_DISPLAY_TYPE_LETTER
654 * @param int $decimals number of decimal places when displaying float values
655 * @return string
657 function grade_format_gradevalue($value, &$grade_item, $localized=true, $displaytype=null, $decimals=null) {
658 if ($grade_item->gradetype == GRADE_TYPE_NONE or $grade_item->gradetype == GRADE_TYPE_TEXT) {
659 return '';
662 // no grade yet?
663 if (is_null($value)) {
664 return '-';
667 if ($grade_item->gradetype != GRADE_TYPE_VALUE and $grade_item->gradetype != GRADE_TYPE_SCALE) {
668 //unknown type??
669 return '';
672 if (is_null($displaytype)) {
673 $displaytype = $grade_item->get_displaytype();
676 if (is_null($decimals)) {
677 $decimals = $grade_item->get_decimals();
680 switch ($displaytype) {
681 case GRADE_DISPLAY_TYPE_REAL:
682 return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized);
684 case GRADE_DISPLAY_TYPE_PERCENTAGE:
685 return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized);
687 case GRADE_DISPLAY_TYPE_LETTER:
688 return grade_format_gradevalue_letter($value, $grade_item);
690 case GRADE_DISPLAY_TYPE_REAL_PERCENTAGE:
691 return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ' (' .
692 grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ')';
694 case GRADE_DISPLAY_TYPE_REAL_LETTER:
695 return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ' (' .
696 grade_format_gradevalue_letter($value, $grade_item) . ')';
698 case GRADE_DISPLAY_TYPE_PERCENTAGE_REAL:
699 return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ' (' .
700 grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ')';
702 case GRADE_DISPLAY_TYPE_LETTER_REAL:
703 return grade_format_gradevalue_letter($value, $grade_item) . ' (' .
704 grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ')';
706 case GRADE_DISPLAY_TYPE_LETTER_PERCENTAGE:
707 return grade_format_gradevalue_letter($value, $grade_item) . ' (' .
708 grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ')';
710 case GRADE_DISPLAY_TYPE_PERCENTAGE_LETTER:
711 return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ' (' .
712 grade_format_gradevalue_letter($value, $grade_item) . ')';
713 default:
714 return '';
719 * @todo Document this function
721 function grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) {
722 if ($grade_item->gradetype == GRADE_TYPE_SCALE) {
723 if (!$scale = $grade_item->load_scale()) {
724 return get_string('error');
727 $value = $grade_item->bounded_grade($value);
728 return format_string($scale->scale_items[$value-1]);
730 } else {
731 return format_float($value, $decimals, $localized);
735 * @todo Document this function
737 function grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) {
738 $min = $grade_item->grademin;
739 $max = $grade_item->grademax;
740 if ($min == $max) {
741 return '';
743 $value = $grade_item->bounded_grade($value);
744 $percentage = (($value-$min)*100)/($max-$min);
745 return format_float($percentage, $decimals, $localized).' %';
748 * @todo Document this function
750 function grade_format_gradevalue_letter($value, $grade_item) {
751 $context = get_context_instance(CONTEXT_COURSE, $grade_item->courseid);
752 if (!$letters = grade_get_letters($context)) {
753 return ''; // no letters??
756 if (is_null($value)) {
757 return '-';
760 $value = grade_grade::standardise_score($value, $grade_item->grademin, $grade_item->grademax, 0, 100);
761 $value = bounded_number(0, $value, 100); // just in case
762 foreach ($letters as $boundary => $letter) {
763 if ($value >= $boundary) {
764 return format_string($letter);
767 return '-'; // no match? maybe '' would be more correct
772 * Returns grade options for gradebook category menu
774 * @param int $courseid
775 * @param bool $includenew include option for new category (-1)
776 * @return array of grade categories in course
778 function grade_get_categories_menu($courseid, $includenew=false) {
779 $result = array();
780 if (!$categories = grade_category::fetch_all(array('courseid'=>$courseid))) {
781 //make sure course category exists
782 if (!grade_category::fetch_course_category($courseid)) {
783 debugging('Can not create course grade category!');
784 return $result;
786 $categories = grade_category::fetch_all(array('courseid'=>$courseid));
788 foreach ($categories as $key=>$category) {
789 if ($category->is_course_category()) {
790 $result[$category->id] = get_string('uncategorised', 'grades');
791 unset($categories[$key]);
794 if ($includenew) {
795 $result[-1] = get_string('newcategory', 'grades');
797 $cats = array();
798 foreach ($categories as $category) {
799 $cats[$category->id] = $category->get_name();
801 textlib_get_instance()->asort($cats);
803 return ($result+$cats);
807 * Returns grade letters array used in context
809 * @param object $context object or null for defaults
810 * @return array of grade_boundary=>letter_string
812 function grade_get_letters($context=null) {
813 global $DB;
815 if (empty($context)) {
816 //default grading letters
817 return array('93'=>'A', '90'=>'A-', '87'=>'B+', '83'=>'B', '80'=>'B-', '77'=>'C+', '73'=>'C', '70'=>'C-', '67'=>'D+', '60'=>'D', '0'=>'F');
820 static $cache = array();
822 if (array_key_exists($context->id, $cache)) {
823 return $cache[$context->id];
826 if (count($cache) > 100) {
827 $cache = array(); // cache size limit
830 $letters = array();
832 $contexts = get_parent_contexts($context);
833 array_unshift($contexts, $context->id);
835 foreach ($contexts as $ctxid) {
836 if ($records = $DB->get_records('grade_letters', array('contextid'=>$ctxid), 'lowerboundary DESC')) {
837 foreach ($records as $record) {
838 $letters[$record->lowerboundary] = $record->letter;
842 if (!empty($letters)) {
843 $cache[$context->id] = $letters;
844 return $letters;
848 $letters = grade_get_letters(null);
849 $cache[$context->id] = $letters;
850 return $letters;
855 * Verify new value of idnumber - checks for uniqueness of new idnumbers, old are kept intact
857 * @global object
858 * @param string idnumber string (with magic quotes)
859 * @param int $courseid id numbers are course unique only
860 * @param object $grade_item is item idnumber
861 * @param object $cm used for course module idnumbers and items attached to modules
862 * @return boolean true means idnumber ok
864 function grade_verify_idnumber($idnumber, $courseid, $grade_item=null, $cm=null) {
865 global $DB;
867 if ($idnumber == '') {
868 //we allow empty idnumbers
869 return true;
872 // keep existing even when not unique
873 if ($cm and $cm->idnumber == $idnumber) {
874 if ($grade_item and $grade_item->itemnumber != 0) {
875 // grade item with itemnumber > 0 can't have the same idnumber as the main
876 // itemnumber 0 which is synced with course_modules
877 return false;
879 return true;
880 } else if ($grade_item and $grade_item->idnumber == $idnumber) {
881 return true;
884 if ($DB->record_exists('course_modules', array('course'=>$courseid, 'idnumber'=>$idnumber))) {
885 return false;
888 if ($DB->record_exists('grade_items', array('courseid'=>$courseid, 'idnumber'=>$idnumber))) {
889 return false;
892 return true;
896 * Force final grade recalculation in all course items
898 * @global object
899 * @param int $courseid
901 function grade_force_full_regrading($courseid) {
902 global $DB;
903 $DB->set_field('grade_items', 'needsupdate', 1, array('courseid'=>$courseid));
907 * Forces regrading of all site grades - usualy when chanign site setings
908 * @global object
909 * @global object
911 function grade_force_site_regrading() {
912 global $CFG, $DB;
913 $DB->set_field('grade_items', 'needsupdate', 1);
917 * Updates all final grades in course.
919 * @param int $courseid
920 * @param int $userid if specified, try to do a quick regrading of grades of this user only
921 * @param object $updated_item the item in which
922 * @return boolean true if ok, array of errors if problems found (item id is used as key)
924 function grade_regrade_final_grades($courseid, $userid=null, $updated_item=null) {
926 $course_item = grade_item::fetch_course_item($courseid);
928 if ($userid) {
929 // one raw grade updated for one user
930 if (empty($updated_item)) {
931 print_error("cannotbenull", 'debug', '', "updated_item");
933 if ($course_item->needsupdate) {
934 $updated_item->force_regrading();
935 return array($course_item->id =>'Can not do fast regrading after updating of raw grades');
938 } else {
939 if (!$course_item->needsupdate) {
940 // nothing to do :-)
941 return true;
945 $grade_items = grade_item::fetch_all(array('courseid'=>$courseid));
946 $depends_on = array();
948 // first mark all category and calculated items as needing regrading
949 // this is slower, but 100% accurate
950 foreach ($grade_items as $gid=>$gitem) {
951 if (!empty($updated_item) and $updated_item->id == $gid) {
952 $grade_items[$gid]->needsupdate = 1;
954 } else if ($gitem->is_course_item() or $gitem->is_category_item() or $gitem->is_calculated()) {
955 $grade_items[$gid]->needsupdate = 1;
958 // construct depends_on lookup array
959 $depends_on[$gid] = $grade_items[$gid]->depends_on();
962 $errors = array();
963 $finalids = array();
964 $gids = array_keys($grade_items);
965 $failed = 0;
967 while (count($finalids) < count($gids)) { // work until all grades are final or error found
968 $count = 0;
969 foreach ($gids as $gid) {
970 if (in_array($gid, $finalids)) {
971 continue; // already final
974 if (!$grade_items[$gid]->needsupdate) {
975 $finalids[] = $gid; // we can make it final - does not need update
976 continue;
979 $doupdate = true;
980 foreach ($depends_on[$gid] as $did) {
981 if (!in_array($did, $finalids)) {
982 $doupdate = false;
983 continue; // this item depends on something that is not yet in finals array
987 //oki - let's update, calculate or aggregate :-)
988 if ($doupdate) {
989 $result = $grade_items[$gid]->regrade_final_grades($userid);
991 if ($result === true) {
992 $grade_items[$gid]->regrading_finished();
993 $grade_items[$gid]->check_locktime(); // do the locktime item locking
994 $count++;
995 $finalids[] = $gid;
997 } else {
998 $grade_items[$gid]->force_regrading();
999 $errors[$gid] = $result;
1004 if ($count == 0) {
1005 $failed++;
1006 } else {
1007 $failed = 0;
1010 if ($failed > 1) {
1011 foreach($gids as $gid) {
1012 if (in_array($gid, $finalids)) {
1013 continue; // this one is ok
1015 $grade_items[$gid]->force_regrading();
1016 $errors[$grade_items[$gid]->id] = 'Probably circular reference or broken calculation formula'; // TODO: localize
1018 break; // oki, found error
1022 if (count($errors) == 0) {
1023 if (empty($userid)) {
1024 // do the locktime locking of grades, but only when doing full regrading
1025 grade_grade::check_locktime_all($gids);
1027 return true;
1028 } else {
1029 return $errors;
1034 * Refetches data from all course activities
1036 * @global object
1037 * @global object
1038 * @param int $courseid
1039 * @param string $modname
1040 * @return void
1042 function grade_grab_course_grades($courseid, $modname=null) {
1043 global $CFG, $DB;
1045 if ($modname) {
1046 $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname
1047 FROM {".$modname."} a, {course_modules} cm, {modules} m
1048 WHERE m.name=:modname AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=:courseid";
1049 $params = array('modname'=>$modname, 'courseid'=>$courseid);
1051 if ($modinstances = $DB->get_records_sql($sql, $params)) {
1052 foreach ($modinstances as $modinstance) {
1053 grade_update_mod_grades($modinstance);
1056 return;
1059 if (!$mods = get_plugin_list('mod') ) {
1060 print_error('nomodules', 'debug');
1063 foreach ($mods as $mod => $fullmod) {
1064 if ($mod == 'NEWMODULE') { // Someone has unzipped the template, ignore it
1065 continue;
1068 // include the module lib once
1069 if (file_exists($fullmod.'/lib.php')) {
1070 // get all instance of the activity
1071 $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname
1072 FROM {".$mod."} a, {course_modules} cm, {modules} m
1073 WHERE m.name=:mod AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=:courseid";
1074 $params = array('mod'=>$mod, 'courseid'=>$courseid);
1076 if ($modinstances = $DB->get_records_sql($sql, $params)) {
1077 foreach ($modinstances as $modinstance) {
1078 grade_update_mod_grades($modinstance);
1086 * Force full update of module grades in central gradebook
1088 * @global object
1089 * @global object
1090 * @param object $modinstance object with extra cmidnumber and modname property
1091 * @param int $userid
1092 * @return boolean success
1094 function grade_update_mod_grades($modinstance, $userid=0) {
1095 global $CFG, $DB;
1097 $fullmod = $CFG->dirroot.'/mod/'.$modinstance->modname;
1098 if (!file_exists($fullmod.'/lib.php')) {
1099 debugging('missing lib.php file in module ' . $modinstance->modname);
1100 return false;
1102 include_once($fullmod.'/lib.php');
1104 $updategradesfunc = $modinstance->modname.'_update_grades';
1105 $updateitemfunc = $modinstance->modname.'_grade_item_update';
1107 if (function_exists($updategradesfunc) and function_exists($updateitemfunc)) {
1108 //new grading supported, force updating of grades
1109 $updateitemfunc($modinstance);
1110 $updategradesfunc($modinstance, $userid);
1112 } else {
1113 // mudule does not support grading??
1116 return true;
1120 * Remove grade letters for given context
1122 * @global object
1123 * @param object $context
1124 * @param bool $showfeedback
1126 function remove_grade_letters($context, $showfeedback) {
1127 global $DB, $OUTPUT;
1129 $strdeleted = get_string('deleted');
1131 $DB->delete_records('grade_letters', array('contextid'=>$context->id));
1132 if ($showfeedback) {
1133 echo $OUTPUT->notification($strdeleted.' - '.get_string('letters', 'grades'));
1137 * Remove all grade related course data - history is kept
1139 * @global object
1140 * @param int $courseid
1141 * @param bool $showfeedback print feedback
1143 function remove_course_grades($courseid, $showfeedback) {
1144 global $DB, $OUTPUT;
1146 $strdeleted = get_string('deleted');
1148 $course_category = grade_category::fetch_course_category($courseid);
1149 $course_category->delete('coursedelete');
1150 if ($showfeedback) {
1151 echo $OUTPUT->notification($strdeleted.' - '.get_string('grades', 'grades').', '.get_string('items', 'grades').', '.get_string('categories', 'grades'));
1154 if ($outcomes = grade_outcome::fetch_all(array('courseid'=>$courseid))) {
1155 foreach ($outcomes as $outcome) {
1156 $outcome->delete('coursedelete');
1159 $DB->delete_records('grade_outcomes_courses', array('courseid'=>$courseid));
1160 if ($showfeedback) {
1161 echo $OUTPUT->notification($strdeleted.' - '.get_string('outcomes', 'grades'));
1164 if ($scales = grade_scale::fetch_all(array('courseid'=>$courseid))) {
1165 foreach ($scales as $scale) {
1166 $scale->delete('coursedelete');
1169 if ($showfeedback) {
1170 echo $OUTPUT->notification($strdeleted.' - '.get_string('scales'));
1173 $DB->delete_records('grade_settings', array('courseid'=>$courseid));
1174 if ($showfeedback) {
1175 echo $OUTPUT->notification($strdeleted.' - '.get_string('settings', 'grades'));
1180 * Called when course category deleted - cleanup gradebook
1182 * @global object
1183 * @param int $categoryid course category id
1184 * @param int $newparentid empty means everything deleted, otherwise id of category where content moved
1185 * @param bool $showfeedback print feedback
1187 function grade_course_category_delete($categoryid, $newparentid, $showfeedback) {
1188 global $DB;
1190 $context = get_context_instance(CONTEXT_COURSECAT, $categoryid);
1191 $DB->delete_records('grade_letters', array('contextid'=>$context->id));
1195 * Does gradebook cleanup when module uninstalled.
1197 * @global object
1198 * @global object
1199 * @param string $modname
1201 function grade_uninstalled_module($modname) {
1202 global $CFG, $DB;
1204 $sql = "SELECT *
1205 FROM {grade_items}
1206 WHERE itemtype='mod' AND itemmodule=?";
1208 // go all items for this module and delete them including the grades
1209 if ($rs = $DB->get_recordset_sql($sql, array($modname))) {
1210 foreach ($rs as $item) {
1211 $grade_item = new grade_item($item, false);
1212 $grade_item->delete('moduninstall');
1214 $rs->close();
1219 * Deletes all user data from gradebook.
1220 * @param $userid
1222 function grade_user_delete($userid) {
1223 if ($grades = grade_grade::fetch_all(array('userid'=>$userid))) {
1224 foreach ($grades as $grade) {
1225 $grade->delete('userdelete');
1231 * Purge course data when user unenrolled.
1232 * @param $userid
1234 function grade_user_unenrol($courseid, $userid) {
1235 if ($items = grade_item::fetch_all(array('courseid'=>$courseid))) {
1236 foreach ($items as $item) {
1237 if ($grades = grade_grade::fetch_all(array('userid'=>$userid, 'itemid'=>$item->id))) {
1238 foreach ($grades as $grade) {
1239 $grade->delete('userdelete');
1247 * Grading cron job
1249 * @global object
1250 * @global object
1252 function grade_cron() {
1253 global $CFG, $DB;
1255 $now = time();
1257 $sql = "SELECT i.*
1258 FROM {grade_items} i
1259 WHERE i.locked = 0 AND i.locktime > 0 AND i.locktime < ? AND EXISTS (
1260 SELECT 'x' FROM {grade_items} c WHERE c.itemtype='course' AND c.needsupdate=0 AND c.courseid=i.courseid)";
1262 // go through all courses that have proper final grades and lock them if needed
1263 if ($rs = $DB->get_recordset_sql($sql, array($now))) {
1264 foreach ($rs as $item) {
1265 $grade_item = new grade_item($item, false);
1266 $grade_item->locked = $now;
1267 $grade_item->update('locktime');
1269 $rs->close();
1272 $grade_inst = new grade_grade();
1273 $fields = 'g.'.implode(',g.', $grade_inst->required_fields);
1275 $sql = "SELECT $fields
1276 FROM {grade_grades} g, {grade_items} i
1277 WHERE g.locked = 0 AND g.locktime > 0 AND g.locktime < ? AND g.itemid=i.id AND EXISTS (
1278 SELECT 'x' FROM {grade_items} c WHERE c.itemtype='course' AND c.needsupdate=0 AND c.courseid=i.courseid)";
1280 // go through all courses that have proper final grades and lock them if needed
1281 if ($rs = $DB->get_recordset_sql($sql, array($now))) {
1282 foreach ($rs as $grade) {
1283 $grade_grade = new grade_grade($grade, false);
1284 $grade_grade->locked = $now;
1285 $grade_grade->update('locktime');
1287 $rs->close();
1290 //TODO: do not run this cleanup every cron invocation
1291 // cleanup history tables
1292 if (!empty($CFG->gradehistorylifetime)) { // value in days
1293 $histlifetime = $now - ($CFG->gradehistorylifetime * 3600 * 24);
1294 $tables = array('grade_outcomes_history', 'grade_categories_history', 'grade_items_history', 'grade_grades_history', 'scale_history');
1295 foreach ($tables as $table) {
1296 if ($DB->delete_records_select($table, "timemodified < ?", array($histlifetime))) {
1297 mtrace(" Deleted old grade history records from '$table'");
1304 * Resel all course grades
1306 * @param int $courseid
1307 * @return bool success
1309 function grade_course_reset($courseid) {
1311 // no recalculations
1312 grade_force_full_regrading($courseid);
1314 $grade_items = grade_item::fetch_all(array('courseid'=>$courseid));
1315 foreach ($grade_items as $gid=>$grade_item) {
1316 $grade_item->delete_all_grades('reset');
1319 //refetch all grades
1320 grade_grab_course_grades($courseid);
1322 // recalculate all grades
1323 grade_regrade_final_grades($courseid);
1324 return true;
1328 * Convert number to 5 decimalfloat, empty string or null db compatible format
1329 * (we need this to decide if db value changed)
1331 * @param mixed $number
1332 * @return mixed float or null
1334 function grade_floatval($number) {
1335 if (is_null($number) or $number === '') {
1336 return null;
1338 // we must round to 5 digits to get the same precision as in 10,5 db fields
1339 // note: db rounding for 10,5 is different from php round() function
1340 return round($number, 5);
1344 * Compare two float numbers safely. Uses 5 decimals php precision. Nulls accepted too.
1345 * Used for skipping of db updates
1347 * @param float $f1
1348 * @param float $f2
1349 * @return bool true if different
1351 function grade_floats_different($f1, $f2) {
1352 // note: db rounding for 10,5 is different from php round() function
1353 return (grade_floatval($f1) !== grade_floatval($f2));
1357 * Compare two float numbers safely. Uses 5 decimals php precision.
1359 * Do not use rounding for 10,5 at the database level as the results may be
1360 * different from php round() function.
1362 * @since 2.0
1363 * @param float $f1
1364 * @param float $f2
1365 * @return bool true if the values should be considered as the same grades
1367 function grade_floats_equal($f1, $f2) {
1368 return (grade_floatval($f1) === grade_floatval($f2));