Merge branch 'wip-mdl-30121-m22' of git://github.com/rajeshtaneja/moodle into MOODLE_...
[moodle.git] / lib / gradelib.php
blob69d294d121151d8689b572ab078331527c8fe1bf
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='uid');
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 collatorlib::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 * Recover a user's grades from grade_grades_history
918 * @param int $userid the user ID whose grades we want to recover
919 * @param int $courseid the relevant course
920 * @return bool true if successful or false if there was an error or no grades could be recovered
922 function grade_recover_history_grades($userid, $courseid) {
923 global $CFG, $DB;
925 if ($CFG->disablegradehistory) {
926 debugging('Attempting to recover grades when grade history is disabled.');
927 return false;
930 //Were grades recovered? Flag to return.
931 $recoveredgrades = false;
933 //Check the user is enrolled in this course
934 //Dont bother checking if they have a gradeable role. They may get one later so recover
935 //whatever grades they have now just in case.
936 $course_context = get_context_instance(CONTEXT_COURSE, $courseid);
937 if (!is_enrolled($course_context, $userid)) {
938 debugging('Attempting to recover the grades of a user who is deleted or not enrolled. Skipping recover.');
939 return false;
942 //Check for existing grades for this user in this course
943 //Recovering grades when the user already has grades can lead to duplicate indexes and bad data
944 //In the future we could move the existing grades to the history table then recover the grades from before then
945 $sql = "SELECT gg.id
946 FROM {grade_grades} gg
947 JOIN {grade_items} gi ON gi.id = gg.itemid
948 WHERE gi.courseid = :courseid AND gg.userid = :userid";
949 $params = array('userid' => $userid, 'courseid' => $courseid);
950 if ($DB->record_exists_sql($sql, $params)) {
951 debugging('Attempting to recover the grades of a user who already has grades. Skipping recover.');
952 return false;
953 } else {
954 //Retrieve the user's old grades
955 //have history ID as first column to guarantee we a unique first column
956 $sql = "SELECT h.id, gi.itemtype, gi.itemmodule, gi.iteminstance as iteminstance, gi.itemnumber, h.source, h.itemid, h.userid, h.rawgrade, h.rawgrademax,
957 h.rawgrademin, h.rawscaleid, h.usermodified, h.finalgrade, h.hidden, h.locked, h.locktime, h.exported, h.overridden, h.excluded, h.feedback,
958 h.feedbackformat, h.information, h.informationformat, h.timemodified, itemcreated.tm AS timecreated
959 FROM {grade_grades_history} h
960 JOIN (SELECT itemid, MAX(id) AS id
961 FROM {grade_grades_history}
962 WHERE userid = :userid1
963 GROUP BY itemid) maxquery ON h.id = maxquery.id AND h.itemid = maxquery.itemid
964 JOIN {grade_items} gi ON gi.id = h.itemid
965 JOIN (SELECT itemid, MAX(timemodified) AS tm
966 FROM {grade_grades_history}
967 WHERE userid = :userid2 AND action = :insertaction
968 GROUP BY itemid) itemcreated ON itemcreated.itemid = h.itemid
969 WHERE gi.courseid = :courseid";
970 $params = array('userid1' => $userid, 'userid2' => $userid , 'insertaction' => GRADE_HISTORY_INSERT, 'courseid' => $courseid);
971 $oldgrades = $DB->get_records_sql($sql, $params);
973 //now move the old grades to the grade_grades table
974 foreach ($oldgrades as $oldgrade) {
975 unset($oldgrade->id);
977 $grade = new grade_grade($oldgrade, false);//2nd arg false as dont want to try and retrieve a record from the DB
978 $grade->insert($oldgrade->source);
980 //dont include default empty grades created when activities are created
981 if (!is_null($oldgrade->finalgrade) || !is_null($oldgrade->feedback)) {
982 $recoveredgrades = true;
987 //Some activities require manual grade synching (moving grades from the activity into the gradebook)
988 //If the student was deleted when synching was done they may have grades in the activity that haven't been moved across
989 grade_grab_course_grades($courseid, null, $userid);
991 return $recoveredgrades;
995 * Updates all final grades in course.
997 * @param int $courseid
998 * @param int $userid if specified, try to do a quick regrading of grades of this user only
999 * @param object $updated_item the item in which
1000 * @return boolean true if ok, array of errors if problems found (item id is used as key)
1002 function grade_regrade_final_grades($courseid, $userid=null, $updated_item=null) {
1004 $course_item = grade_item::fetch_course_item($courseid);
1006 if ($userid) {
1007 // one raw grade updated for one user
1008 if (empty($updated_item)) {
1009 print_error("cannotbenull", 'debug', '', "updated_item");
1011 if ($course_item->needsupdate) {
1012 $updated_item->force_regrading();
1013 return array($course_item->id =>'Can not do fast regrading after updating of raw grades');
1016 } else {
1017 if (!$course_item->needsupdate) {
1018 // nothing to do :-)
1019 return true;
1023 $grade_items = grade_item::fetch_all(array('courseid'=>$courseid));
1024 $depends_on = array();
1026 // first mark all category and calculated items as needing regrading
1027 // this is slower, but 100% accurate
1028 foreach ($grade_items as $gid=>$gitem) {
1029 if (!empty($updated_item) and $updated_item->id == $gid) {
1030 $grade_items[$gid]->needsupdate = 1;
1032 } else if ($gitem->is_course_item() or $gitem->is_category_item() or $gitem->is_calculated()) {
1033 $grade_items[$gid]->needsupdate = 1;
1036 // construct depends_on lookup array
1037 $depends_on[$gid] = $grade_items[$gid]->depends_on();
1040 $errors = array();
1041 $finalids = array();
1042 $gids = array_keys($grade_items);
1043 $failed = 0;
1045 while (count($finalids) < count($gids)) { // work until all grades are final or error found
1046 $count = 0;
1047 foreach ($gids as $gid) {
1048 if (in_array($gid, $finalids)) {
1049 continue; // already final
1052 if (!$grade_items[$gid]->needsupdate) {
1053 $finalids[] = $gid; // we can make it final - does not need update
1054 continue;
1057 $doupdate = true;
1058 foreach ($depends_on[$gid] as $did) {
1059 if (!in_array($did, $finalids)) {
1060 $doupdate = false;
1061 continue; // this item depends on something that is not yet in finals array
1065 //oki - let's update, calculate or aggregate :-)
1066 if ($doupdate) {
1067 $result = $grade_items[$gid]->regrade_final_grades($userid);
1069 if ($result === true) {
1070 $grade_items[$gid]->regrading_finished();
1071 $grade_items[$gid]->check_locktime(); // do the locktime item locking
1072 $count++;
1073 $finalids[] = $gid;
1075 } else {
1076 $grade_items[$gid]->force_regrading();
1077 $errors[$gid] = $result;
1082 if ($count == 0) {
1083 $failed++;
1084 } else {
1085 $failed = 0;
1088 if ($failed > 1) {
1089 foreach($gids as $gid) {
1090 if (in_array($gid, $finalids)) {
1091 continue; // this one is ok
1093 $grade_items[$gid]->force_regrading();
1094 $errors[$grade_items[$gid]->id] = 'Probably circular reference or broken calculation formula'; // TODO: localize
1096 break; // oki, found error
1100 if (count($errors) == 0) {
1101 if (empty($userid)) {
1102 // do the locktime locking of grades, but only when doing full regrading
1103 grade_grade::check_locktime_all($gids);
1105 return true;
1106 } else {
1107 return $errors;
1112 * Refetches data from all course activities
1113 * @param int $courseid the course ID
1114 * @param string $modname limit the grade fetch to a single module type
1115 * @param int $userid limit the grade fetch to a single user
1116 * @return void
1118 function grade_grab_course_grades($courseid, $modname=null, $userid=0) {
1119 global $CFG, $DB;
1121 if ($modname) {
1122 $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname
1123 FROM {".$modname."} a, {course_modules} cm, {modules} m
1124 WHERE m.name=:modname AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=:courseid";
1125 $params = array('modname'=>$modname, 'courseid'=>$courseid);
1127 if ($modinstances = $DB->get_records_sql($sql, $params)) {
1128 foreach ($modinstances as $modinstance) {
1129 grade_update_mod_grades($modinstance, $userid);
1132 return;
1135 if (!$mods = get_plugin_list('mod') ) {
1136 print_error('nomodules', 'debug');
1139 foreach ($mods as $mod => $fullmod) {
1140 if ($mod == 'NEWMODULE') { // Someone has unzipped the template, ignore it
1141 continue;
1144 // include the module lib once
1145 if (file_exists($fullmod.'/lib.php')) {
1146 // get all instance of the activity
1147 $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname
1148 FROM {".$mod."} a, {course_modules} cm, {modules} m
1149 WHERE m.name=:mod AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=:courseid";
1150 $params = array('mod'=>$mod, 'courseid'=>$courseid);
1152 if ($modinstances = $DB->get_records_sql($sql, $params)) {
1153 foreach ($modinstances as $modinstance) {
1154 grade_update_mod_grades($modinstance, $userid);
1162 * Force full update of module grades in central gradebook
1164 * @global object
1165 * @global object
1166 * @param object $modinstance object with extra cmidnumber and modname property
1167 * @param int $userid
1168 * @return boolean success
1170 function grade_update_mod_grades($modinstance, $userid=0) {
1171 global $CFG, $DB;
1173 $fullmod = $CFG->dirroot.'/mod/'.$modinstance->modname;
1174 if (!file_exists($fullmod.'/lib.php')) {
1175 debugging('missing lib.php file in module ' . $modinstance->modname);
1176 return false;
1178 include_once($fullmod.'/lib.php');
1180 $updategradesfunc = $modinstance->modname.'_update_grades';
1181 $updateitemfunc = $modinstance->modname.'_grade_item_update';
1183 if (function_exists($updategradesfunc) and function_exists($updateitemfunc)) {
1184 //new grading supported, force updating of grades
1185 $updateitemfunc($modinstance);
1186 $updategradesfunc($modinstance, $userid);
1188 } else {
1189 // mudule does not support grading??
1192 return true;
1196 * Remove grade letters for given context
1198 * @param context $context
1199 * @param bool $showfeedback
1201 function remove_grade_letters($context, $showfeedback) {
1202 global $DB, $OUTPUT;
1204 $strdeleted = get_string('deleted');
1206 $DB->delete_records('grade_letters', array('contextid'=>$context->id));
1207 if ($showfeedback) {
1208 echo $OUTPUT->notification($strdeleted.' - '.get_string('letters', 'grades'), 'notifysuccess');
1213 * Remove all grade related course data - history is kept
1215 * @param int $courseid
1216 * @param bool $showfeedback print feedback
1218 function remove_course_grades($courseid, $showfeedback) {
1219 global $DB, $OUTPUT;
1221 $fs = get_file_storage();
1222 $strdeleted = get_string('deleted');
1224 $course_category = grade_category::fetch_course_category($courseid);
1225 $course_category->delete('coursedelete');
1226 $fs->delete_area_files(get_context_instance(CONTEXT_COURSE, $courseid)->id, 'grade', 'feedback');
1227 if ($showfeedback) {
1228 echo $OUTPUT->notification($strdeleted.' - '.get_string('grades', 'grades').', '.get_string('items', 'grades').', '.get_string('categories', 'grades'), 'notifysuccess');
1231 if ($outcomes = grade_outcome::fetch_all(array('courseid'=>$courseid))) {
1232 foreach ($outcomes as $outcome) {
1233 $outcome->delete('coursedelete');
1236 $DB->delete_records('grade_outcomes_courses', array('courseid'=>$courseid));
1237 if ($showfeedback) {
1238 echo $OUTPUT->notification($strdeleted.' - '.get_string('outcomes', 'grades'), 'notifysuccess');
1241 if ($scales = grade_scale::fetch_all(array('courseid'=>$courseid))) {
1242 foreach ($scales as $scale) {
1243 $scale->delete('coursedelete');
1246 if ($showfeedback) {
1247 echo $OUTPUT->notification($strdeleted.' - '.get_string('scales'), 'notifysuccess');
1250 $DB->delete_records('grade_settings', array('courseid'=>$courseid));
1251 if ($showfeedback) {
1252 echo $OUTPUT->notification($strdeleted.' - '.get_string('settings', 'grades'), 'notifysuccess');
1257 * Called when course category deleted - cleanup gradebook
1259 * @global object
1260 * @param int $categoryid course category id
1261 * @param int $newparentid empty means everything deleted, otherwise id of category where content moved
1262 * @param bool $showfeedback print feedback
1264 function grade_course_category_delete($categoryid, $newparentid, $showfeedback) {
1265 global $DB;
1267 $context = get_context_instance(CONTEXT_COURSECAT, $categoryid);
1268 $DB->delete_records('grade_letters', array('contextid'=>$context->id));
1272 * Does gradebook cleanup when module uninstalled.
1274 * @global object
1275 * @global object
1276 * @param string $modname
1278 function grade_uninstalled_module($modname) {
1279 global $CFG, $DB;
1281 $sql = "SELECT *
1282 FROM {grade_items}
1283 WHERE itemtype='mod' AND itemmodule=?";
1285 // go all items for this module and delete them including the grades
1286 $rs = $DB->get_recordset_sql($sql, array($modname));
1287 foreach ($rs as $item) {
1288 $grade_item = new grade_item($item, false);
1289 $grade_item->delete('moduninstall');
1291 $rs->close();
1295 * Deletes all user data from gradebook.
1296 * @param $userid
1298 function grade_user_delete($userid) {
1299 if ($grades = grade_grade::fetch_all(array('userid'=>$userid))) {
1300 foreach ($grades as $grade) {
1301 $grade->delete('userdelete');
1307 * Purge course data when user unenrolled.
1308 * @param $userid
1310 function grade_user_unenrol($courseid, $userid) {
1311 if ($items = grade_item::fetch_all(array('courseid'=>$courseid))) {
1312 foreach ($items as $item) {
1313 if ($grades = grade_grade::fetch_all(array('userid'=>$userid, 'itemid'=>$item->id))) {
1314 foreach ($grades as $grade) {
1315 $grade->delete('userdelete');
1323 * Grading cron job
1325 * @global object
1326 * @global object
1328 function grade_cron() {
1329 global $CFG, $DB;
1331 $now = time();
1333 $sql = "SELECT i.*
1334 FROM {grade_items} i
1335 WHERE i.locked = 0 AND i.locktime > 0 AND i.locktime < ? AND EXISTS (
1336 SELECT 'x' FROM {grade_items} c WHERE c.itemtype='course' AND c.needsupdate=0 AND c.courseid=i.courseid)";
1338 // go through all courses that have proper final grades and lock them if needed
1339 $rs = $DB->get_recordset_sql($sql, array($now));
1340 foreach ($rs as $item) {
1341 $grade_item = new grade_item($item, false);
1342 $grade_item->locked = $now;
1343 $grade_item->update('locktime');
1345 $rs->close();
1347 $grade_inst = new grade_grade();
1348 $fields = 'g.'.implode(',g.', $grade_inst->required_fields);
1350 $sql = "SELECT $fields
1351 FROM {grade_grades} g, {grade_items} i
1352 WHERE g.locked = 0 AND g.locktime > 0 AND g.locktime < ? AND g.itemid=i.id AND EXISTS (
1353 SELECT 'x' FROM {grade_items} c WHERE c.itemtype='course' AND c.needsupdate=0 AND c.courseid=i.courseid)";
1355 // go through all courses that have proper final grades and lock them if needed
1356 $rs = $DB->get_recordset_sql($sql, array($now));
1357 foreach ($rs as $grade) {
1358 $grade_grade = new grade_grade($grade, false);
1359 $grade_grade->locked = $now;
1360 $grade_grade->update('locktime');
1362 $rs->close();
1364 //TODO: do not run this cleanup every cron invocation
1365 // cleanup history tables
1366 if (!empty($CFG->gradehistorylifetime)) { // value in days
1367 $histlifetime = $now - ($CFG->gradehistorylifetime * 3600 * 24);
1368 $tables = array('grade_outcomes_history', 'grade_categories_history', 'grade_items_history', 'grade_grades_history', 'scale_history');
1369 foreach ($tables as $table) {
1370 if ($DB->delete_records_select($table, "timemodified < ?", array($histlifetime))) {
1371 mtrace(" Deleted old grade history records from '$table'");
1378 * Resel all course grades
1380 * @param int $courseid
1381 * @return bool success
1383 function grade_course_reset($courseid) {
1385 // no recalculations
1386 grade_force_full_regrading($courseid);
1388 $grade_items = grade_item::fetch_all(array('courseid'=>$courseid));
1389 foreach ($grade_items as $gid=>$grade_item) {
1390 $grade_item->delete_all_grades('reset');
1393 //refetch all grades
1394 grade_grab_course_grades($courseid);
1396 // recalculate all grades
1397 grade_regrade_final_grades($courseid);
1398 return true;
1402 * Convert number to 5 decimalfloat, empty string or null db compatible format
1403 * (we need this to decide if db value changed)
1405 * @param mixed $number
1406 * @return mixed float or null
1408 function grade_floatval($number) {
1409 if (is_null($number) or $number === '') {
1410 return null;
1412 // we must round to 5 digits to get the same precision as in 10,5 db fields
1413 // note: db rounding for 10,5 is different from php round() function
1414 return round($number, 5);
1418 * Compare two float numbers safely. Uses 5 decimals php precision. Nulls accepted too.
1419 * Used for skipping of db updates
1421 * @param float $f1
1422 * @param float $f2
1423 * @return bool true if different
1425 function grade_floats_different($f1, $f2) {
1426 // note: db rounding for 10,5 is different from php round() function
1427 return (grade_floatval($f1) !== grade_floatval($f2));
1431 * Compare two float numbers safely. Uses 5 decimals php precision.
1433 * Do not use rounding for 10,5 at the database level as the results may be
1434 * different from php round() function.
1436 * @since 2.0
1437 * @param float $f1
1438 * @param float $f2
1439 * @return bool true if the values should be considered as the same grades
1441 function grade_floats_equal($f1, $f2) {
1442 return (grade_floatval($f1) === grade_floatval($f2));