Merge branch 'MDL-32903' of git://github.com/netspotau/moodle-mod_assign
[moodle.git] / lib / gradelib.php
blob8de67cf103d7bc2e5274325e8063c7843050c6a7
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17 /**
18 * Library of functions for gradebook - both public and internal
20 * @package core_grades
21 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 defined('MOODLE_INTERNAL') || die();
27 /** 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 /////////////////////////////////////////////////////////////////////
40 /**
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.
50 * @category grade
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)) {
78 // create a new one
79 $grade_item = false;
81 } else if (count($grade_items) == 1){
82 $grade_item = reset($grade_items);
83 unset($grade_items); //release memory
85 } else {
86 debugging('Found more than one grade item');
87 return GRADE_UPDATE_MULTIPLE;
90 if (!empty($itemdetails['deleted'])) {
91 if ($grade_item) {
92 if ($grade_item->delete($source)) {
93 return GRADE_UPDATE_OK;
94 } else {
95 return GRADE_UPDATE_FAILED;
98 return GRADE_UPDATE_OK;
101 /// Create or update the grade_item if needed
103 if (!$grade_item) {
104 if ($itemdetails) {
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)) {
117 // ignore it
118 continue;
120 if ($k == 'gradetype' and $v == GRADE_TYPE_NONE) {
121 // no grade item needed!
122 return GRADE_UPDATE_OK;
124 $params[$k] = $v;
127 $grade_item = new grade_item($params);
128 $grade_item->insert();
130 } else {
131 if ($grade_item->is_locked()) {
132 // no notice() here, test returned value instead!
133 return GRADE_UPDATE_ITEM_LOCKED;
136 if ($itemdetails) {
137 $itemdetails = (array)$itemdetails;
138 $update = false;
139 foreach ($itemdetails as $k=>$v) {
140 if (!in_array($k, $allowed)) {
141 // ignore it
142 continue;
144 if (in_array($k, $floats)) {
145 if (grade_floats_different($grade_item->{$k}, $v)) {
146 $grade_item->{$k} = $v;
147 $update = true;
150 } else {
151 if ($grade_item->{$k} != $v) {
152 $grade_item->{$k} = $v;
153 $update = true;
157 if ($update) {
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);
183 } else {
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) {
191 if (!is_array($g)) {
192 $g = (array)$g;
193 $grades[$k] = $g;
196 if (empty($g['userid']) or $k != $g['userid']) {
197 debugging('Incorrect grade array index, must be user id! Grade ignored.');
198 unset($grades[$k]);
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";
212 } else {
213 $sql = "SELECT * FROM {grade_grades} WHERE itemid = :gid";
214 $params = array('gid'=>$grade_item->id);
217 $rs = $DB->get_recordset_sql($sql, $params);
219 $failed = false;
221 while (count($grades) > 0) {
222 $grade_grade = null;
223 $grade = null;
225 foreach ($rs as $gd) {
227 $userid = $gd->userid;
228 if (!isset($grades[$userid])) {
229 // this grade not requested, continue
230 continue;
232 // existing grade requested
233 $grade = $grades[$userid];
234 $grade_grade = new grade_grade($gd, false);
235 unset($grades[$userid]);
236 break;
239 if (is_null($grade_grade)) {
240 if (count($grades) == 0) {
241 // no more grades to process
242 break;
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]);
252 $rawgrade = false;
253 $feedback = false;
254 $feedbackformat = FORMAT_MOODLE;
255 $usermodified = $USER->id;
256 $datesubmitted = null;
257 $dategraded = 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)) {
285 $failed = true;
289 if ($rs) {
290 $rs->close();
293 if (!$failed) {
294 return GRADE_UPDATE_OK;
295 } else {
296 return GRADE_UPDATE_FAILED;
301 * Updates a user's outcomes. Manual outcomes can not be updated.
303 * @category grade
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))) {
315 $result = true;
316 foreach ($items as $item) {
317 if (!array_key_exists($item->itemnumber, $data)) {
318 continue;
320 $grade = $data[$item->itemnumber] < 1 ? null : $data[$item->itemnumber];
321 $result = ($item->update_final_grade($userid, $grade, $source) && $result);
323 return $result;
325 return false; //grade items not found
329 * Returns grading information for given activity, optionally with user grades
330 * Manual, course or category items can not be queried.
332 * @category grade
333 * @param int $courseid ID of course
334 * @param string $itemtype Type of grade item. For example, 'mod' or 'block'
335 * @param string $itemmodule More specific then $itemtype. For example, 'forum' or 'quiz'. May be NULL for some item types
336 * @param int $iteminstance ID of the item module
337 * @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
338 * @return array Array of grade information objects (scaleid, name, grade and locked status, etc.) indexed with itemnumbers
340 function grade_get_grades($courseid, $itemtype, $itemmodule, $iteminstance, $userid_or_ids=null) {
341 global $CFG;
343 $return = new stdClass();
344 $return->items = array();
345 $return->outcomes = array();
347 $course_item = grade_item::fetch_course_item($courseid);
348 $needsupdate = array();
349 if ($course_item->needsupdate) {
350 $result = grade_regrade_final_grades($courseid);
351 if ($result !== true) {
352 $needsupdate = array_keys($result);
356 if ($grade_items = grade_item::fetch_all(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid))) {
357 foreach ($grade_items as $grade_item) {
358 $decimalpoints = null;
360 if (empty($grade_item->outcomeid)) {
361 // prepare information about grade item
362 $item = new stdClass();
363 $item->itemnumber = $grade_item->itemnumber;
364 $item->scaleid = $grade_item->scaleid;
365 $item->name = $grade_item->get_name();
366 $item->grademin = $grade_item->grademin;
367 $item->grademax = $grade_item->grademax;
368 $item->gradepass = $grade_item->gradepass;
369 $item->locked = $grade_item->is_locked();
370 $item->hidden = $grade_item->is_hidden();
371 $item->grades = array();
373 switch ($grade_item->gradetype) {
374 case GRADE_TYPE_NONE:
375 continue;
377 case GRADE_TYPE_VALUE:
378 $item->scaleid = 0;
379 break;
381 case GRADE_TYPE_TEXT:
382 $item->scaleid = 0;
383 $item->grademin = 0;
384 $item->grademax = 0;
385 $item->gradepass = 0;
386 break;
389 if (empty($userid_or_ids)) {
390 $userids = array();
392 } else if (is_array($userid_or_ids)) {
393 $userids = $userid_or_ids;
395 } else {
396 $userids = array($userid_or_ids);
399 if ($userids) {
400 $grade_grades = grade_grade::fetch_users_grades($grade_item, $userids, true);
401 foreach ($userids as $userid) {
402 $grade_grades[$userid]->grade_item =& $grade_item;
404 $grade = new stdClass();
405 $grade->grade = $grade_grades[$userid]->finalgrade;
406 $grade->locked = $grade_grades[$userid]->is_locked();
407 $grade->hidden = $grade_grades[$userid]->is_hidden();
408 $grade->overridden = $grade_grades[$userid]->overridden;
409 $grade->feedback = $grade_grades[$userid]->feedback;
410 $grade->feedbackformat = $grade_grades[$userid]->feedbackformat;
411 $grade->usermodified = $grade_grades[$userid]->usermodified;
412 $grade->datesubmitted = $grade_grades[$userid]->get_datesubmitted();
413 $grade->dategraded = $grade_grades[$userid]->get_dategraded();
415 // create text representation of grade
416 if ($grade_item->gradetype == GRADE_TYPE_TEXT or $grade_item->gradetype == GRADE_TYPE_NONE) {
417 $grade->grade = null;
418 $grade->str_grade = '-';
419 $grade->str_long_grade = $grade->str_grade;
421 } else if (in_array($grade_item->id, $needsupdate)) {
422 $grade->grade = false;
423 $grade->str_grade = get_string('error');
424 $grade->str_long_grade = $grade->str_grade;
426 } else if (is_null($grade->grade)) {
427 $grade->str_grade = '-';
428 $grade->str_long_grade = $grade->str_grade;
430 } else {
431 $grade->str_grade = grade_format_gradevalue($grade->grade, $grade_item);
432 if ($grade_item->gradetype == GRADE_TYPE_SCALE or $grade_item->get_displaytype() != GRADE_DISPLAY_TYPE_REAL) {
433 $grade->str_long_grade = $grade->str_grade;
434 } else {
435 $a = new stdClass();
436 $a->grade = $grade->str_grade;
437 $a->max = grade_format_gradevalue($grade_item->grademax, $grade_item);
438 $grade->str_long_grade = get_string('gradelong', 'grades', $a);
442 // create html representation of feedback
443 if (is_null($grade->feedback)) {
444 $grade->str_feedback = '';
445 } else {
446 $grade->str_feedback = format_text($grade->feedback, $grade->feedbackformat);
449 $item->grades[$userid] = $grade;
452 $return->items[$grade_item->itemnumber] = $item;
454 } else {
455 if (!$grade_outcome = grade_outcome::fetch(array('id'=>$grade_item->outcomeid))) {
456 debugging('Incorect outcomeid found');
457 continue;
460 // outcome info
461 $outcome = new stdClass();
462 $outcome->itemnumber = $grade_item->itemnumber;
463 $outcome->scaleid = $grade_outcome->scaleid;
464 $outcome->name = $grade_outcome->get_name();
465 $outcome->locked = $grade_item->is_locked();
466 $outcome->hidden = $grade_item->is_hidden();
468 if (empty($userid_or_ids)) {
469 $userids = array();
470 } else if (is_array($userid_or_ids)) {
471 $userids = $userid_or_ids;
472 } else {
473 $userids = array($userid_or_ids);
476 if ($userids) {
477 $grade_grades = grade_grade::fetch_users_grades($grade_item, $userids, true);
478 foreach ($userids as $userid) {
479 $grade_grades[$userid]->grade_item =& $grade_item;
481 $grade = new stdClass();
482 $grade->grade = $grade_grades[$userid]->finalgrade;
483 $grade->locked = $grade_grades[$userid]->is_locked();
484 $grade->hidden = $grade_grades[$userid]->is_hidden();
485 $grade->feedback = $grade_grades[$userid]->feedback;
486 $grade->feedbackformat = $grade_grades[$userid]->feedbackformat;
487 $grade->usermodified = $grade_grades[$userid]->usermodified;
489 // create text representation of grade
490 if (in_array($grade_item->id, $needsupdate)) {
491 $grade->grade = false;
492 $grade->str_grade = get_string('error');
494 } else if (is_null($grade->grade)) {
495 $grade->grade = 0;
496 $grade->str_grade = get_string('nooutcome', 'grades');
498 } else {
499 $grade->grade = (int)$grade->grade;
500 $scale = $grade_item->load_scale();
501 $grade->str_grade = format_string($scale->scale_items[(int)$grade->grade-1]);
504 // create html representation of feedback
505 if (is_null($grade->feedback)) {
506 $grade->str_feedback = '';
507 } else {
508 $grade->str_feedback = format_text($grade->feedback, $grade->feedbackformat);
511 $outcome->grades[$userid] = $grade;
515 if (isset($return->outcomes[$grade_item->itemnumber])) {
516 // itemnumber duplicates - lets fix them!
517 $newnumber = $grade_item->itemnumber + 1;
518 while(grade_item::fetch(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid, 'itemnumber'=>$newnumber))) {
519 $newnumber++;
521 $outcome->itemnumber = $newnumber;
522 $grade_item->itemnumber = $newnumber;
523 $grade_item->update('system');
526 $return->outcomes[$grade_item->itemnumber] = $outcome;
532 // sort results using itemnumbers
533 ksort($return->items, SORT_NUMERIC);
534 ksort($return->outcomes, SORT_NUMERIC);
536 return $return;
539 ///////////////////////////////////////////////////////////////////
540 ///// End of public API for communication with modules/blocks /////
541 ///////////////////////////////////////////////////////////////////
545 ///////////////////////////////////////////////////////////////////
546 ///// Internal API: used by gradebook plugins and Moodle core /////
547 ///////////////////////////////////////////////////////////////////
550 * Returns a course gradebook setting
552 * @param int $courseid
553 * @param string $name of setting, maybe null if reset only
554 * @param string $default value to return if setting is not found
555 * @param bool $resetcache force reset of internal static cache
556 * @return string value of the setting, $default if setting not found, NULL if supplied $name is null
558 function grade_get_setting($courseid, $name, $default=null, $resetcache=false) {
559 global $DB;
561 static $cache = array();
563 if ($resetcache or !array_key_exists($courseid, $cache)) {
564 $cache[$courseid] = array();
566 } else if (is_null($name)) {
567 return null;
569 } else if (array_key_exists($name, $cache[$courseid])) {
570 return $cache[$courseid][$name];
573 if (!$data = $DB->get_record('grade_settings', array('courseid'=>$courseid, 'name'=>$name))) {
574 $result = null;
575 } else {
576 $result = $data->value;
579 if (is_null($result)) {
580 $result = $default;
583 $cache[$courseid][$name] = $result;
584 return $result;
588 * Returns all course gradebook settings as object properties
590 * @param int $courseid
591 * @return object
593 function grade_get_settings($courseid) {
594 global $DB;
596 $settings = new stdClass();
597 $settings->id = $courseid;
599 if ($records = $DB->get_records('grade_settings', array('courseid'=>$courseid))) {
600 foreach ($records as $record) {
601 $settings->{$record->name} = $record->value;
605 return $settings;
609 * Add, update or delete a course gradebook setting
611 * @param int $courseid The course ID
612 * @param string $name Name of the setting
613 * @param string $value Value of the setting. NULL means delete the setting.
615 function grade_set_setting($courseid, $name, $value) {
616 global $DB;
618 if (is_null($value)) {
619 $DB->delete_records('grade_settings', array('courseid'=>$courseid, 'name'=>$name));
621 } else if (!$existing = $DB->get_record('grade_settings', array('courseid'=>$courseid, 'name'=>$name))) {
622 $data = new stdClass();
623 $data->courseid = $courseid;
624 $data->name = $name;
625 $data->value = $value;
626 $DB->insert_record('grade_settings', $data);
628 } else {
629 $data = new stdClass();
630 $data->id = $existing->id;
631 $data->value = $value;
632 $DB->update_record('grade_settings', $data);
635 grade_get_setting($courseid, null, null, true); // reset the cache
639 * Returns string representation of grade value
641 * @param float $value The grade value
642 * @param object $grade_item Grade item object passed by reference to prevent scale reloading
643 * @param bool $localized use localised decimal separator
644 * @param int $displaytype type of display. For example GRADE_DISPLAY_TYPE_REAL, GRADE_DISPLAY_TYPE_PERCENTAGE, GRADE_DISPLAY_TYPE_LETTER
645 * @param int $decimals The number of decimal places when displaying float values
646 * @return string
648 function grade_format_gradevalue($value, &$grade_item, $localized=true, $displaytype=null, $decimals=null) {
649 if ($grade_item->gradetype == GRADE_TYPE_NONE or $grade_item->gradetype == GRADE_TYPE_TEXT) {
650 return '';
653 // no grade yet?
654 if (is_null($value)) {
655 return '-';
658 if ($grade_item->gradetype != GRADE_TYPE_VALUE and $grade_item->gradetype != GRADE_TYPE_SCALE) {
659 //unknown type??
660 return '';
663 if (is_null($displaytype)) {
664 $displaytype = $grade_item->get_displaytype();
667 if (is_null($decimals)) {
668 $decimals = $grade_item->get_decimals();
671 switch ($displaytype) {
672 case GRADE_DISPLAY_TYPE_REAL:
673 return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized);
675 case GRADE_DISPLAY_TYPE_PERCENTAGE:
676 return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized);
678 case GRADE_DISPLAY_TYPE_LETTER:
679 return grade_format_gradevalue_letter($value, $grade_item);
681 case GRADE_DISPLAY_TYPE_REAL_PERCENTAGE:
682 return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ' (' .
683 grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ')';
685 case GRADE_DISPLAY_TYPE_REAL_LETTER:
686 return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ' (' .
687 grade_format_gradevalue_letter($value, $grade_item) . ')';
689 case GRADE_DISPLAY_TYPE_PERCENTAGE_REAL:
690 return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ' (' .
691 grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ')';
693 case GRADE_DISPLAY_TYPE_LETTER_REAL:
694 return grade_format_gradevalue_letter($value, $grade_item) . ' (' .
695 grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ')';
697 case GRADE_DISPLAY_TYPE_LETTER_PERCENTAGE:
698 return grade_format_gradevalue_letter($value, $grade_item) . ' (' .
699 grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ')';
701 case GRADE_DISPLAY_TYPE_PERCENTAGE_LETTER:
702 return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ' (' .
703 grade_format_gradevalue_letter($value, $grade_item) . ')';
704 default:
705 return '';
710 * Returns a float representation of a grade value
712 * @param float $value The grade value
713 * @param object $grade_item Grade item object
714 * @param int $decimals The number of decimal places
715 * @param bool $localized use localised decimal separator
716 * @return string
718 function grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) {
719 if ($grade_item->gradetype == GRADE_TYPE_SCALE) {
720 if (!$scale = $grade_item->load_scale()) {
721 return get_string('error');
724 $value = $grade_item->bounded_grade($value);
725 return format_string($scale->scale_items[$value-1]);
727 } else {
728 return format_float($value, $decimals, $localized);
733 * Returns a percentage representation of a grade value
735 * @param float $value The grade value
736 * @param object $grade_item Grade item object
737 * @param int $decimals The number of decimal places
738 * @param bool $localized use localised decimal separator
739 * @return string
741 function grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) {
742 $min = $grade_item->grademin;
743 $max = $grade_item->grademax;
744 if ($min == $max) {
745 return '';
747 $value = $grade_item->bounded_grade($value);
748 $percentage = (($value-$min)*100)/($max-$min);
749 return format_float($percentage, $decimals, $localized).' %';
753 * Returns a letter grade representation of a grade value
754 * The array of grade letters used is produced by {@link grade_get_letters()} using the course context
756 * @param float $value The grade value
757 * @param object $grade_item Grade item object
758 * @return string
760 function grade_format_gradevalue_letter($value, $grade_item) {
761 $context = get_context_instance(CONTEXT_COURSE, $grade_item->courseid);
762 if (!$letters = grade_get_letters($context)) {
763 return ''; // no letters??
766 if (is_null($value)) {
767 return '-';
770 $value = grade_grade::standardise_score($value, $grade_item->grademin, $grade_item->grademax, 0, 100);
771 $value = bounded_number(0, $value, 100); // just in case
772 foreach ($letters as $boundary => $letter) {
773 if ($value >= $boundary) {
774 return format_string($letter);
777 return '-'; // no match? maybe '' would be more correct
782 * Returns grade options for gradebook grade category menu
784 * @param int $courseid The course ID
785 * @param bool $includenew Include option for new category at array index -1
786 * @return array of grade categories in course
788 function grade_get_categories_menu($courseid, $includenew=false) {
789 $result = array();
790 if (!$categories = grade_category::fetch_all(array('courseid'=>$courseid))) {
791 //make sure course category exists
792 if (!grade_category::fetch_course_category($courseid)) {
793 debugging('Can not create course grade category!');
794 return $result;
796 $categories = grade_category::fetch_all(array('courseid'=>$courseid));
798 foreach ($categories as $key=>$category) {
799 if ($category->is_course_category()) {
800 $result[$category->id] = get_string('uncategorised', 'grades');
801 unset($categories[$key]);
804 if ($includenew) {
805 $result[-1] = get_string('newcategory', 'grades');
807 $cats = array();
808 foreach ($categories as $category) {
809 $cats[$category->id] = $category->get_name();
811 collatorlib::asort($cats);
813 return ($result+$cats);
817 * Returns the array of grade letters to be used in the supplied context
819 * @param object $context Context object or null for defaults
820 * @return array of grade_boundary (minimum) => letter_string
822 function grade_get_letters($context=null) {
823 global $DB;
825 if (empty($context)) {
826 //default grading letters
827 return array('93'=>'A', '90'=>'A-', '87'=>'B+', '83'=>'B', '80'=>'B-', '77'=>'C+', '73'=>'C', '70'=>'C-', '67'=>'D+', '60'=>'D', '0'=>'F');
830 static $cache = array();
832 if (array_key_exists($context->id, $cache)) {
833 return $cache[$context->id];
836 if (count($cache) > 100) {
837 $cache = array(); // cache size limit
840 $letters = array();
842 $contexts = get_parent_contexts($context);
843 array_unshift($contexts, $context->id);
845 foreach ($contexts as $ctxid) {
846 if ($records = $DB->get_records('grade_letters', array('contextid'=>$ctxid), 'lowerboundary DESC')) {
847 foreach ($records as $record) {
848 $letters[$record->lowerboundary] = $record->letter;
852 if (!empty($letters)) {
853 $cache[$context->id] = $letters;
854 return $letters;
858 $letters = grade_get_letters(null);
859 $cache[$context->id] = $letters;
860 return $letters;
865 * Verify new value of grade item idnumber. Checks for uniqueness of new ID numbers. Old ID numbers are kept intact.
867 * @param string $idnumber string (with magic quotes)
868 * @param int $courseid ID numbers are course unique only
869 * @param grade_item $grade_item The grade item this idnumber is associated with
870 * @param stdClass $cm used for course module idnumbers and items attached to modules
871 * @return bool true means idnumber ok
873 function grade_verify_idnumber($idnumber, $courseid, $grade_item=null, $cm=null) {
874 global $DB;
876 if ($idnumber == '') {
877 //we allow empty idnumbers
878 return true;
881 // keep existing even when not unique
882 if ($cm and $cm->idnumber == $idnumber) {
883 if ($grade_item and $grade_item->itemnumber != 0) {
884 // grade item with itemnumber > 0 can't have the same idnumber as the main
885 // itemnumber 0 which is synced with course_modules
886 return false;
888 return true;
889 } else if ($grade_item and $grade_item->idnumber == $idnumber) {
890 return true;
893 if ($DB->record_exists('course_modules', array('course'=>$courseid, 'idnumber'=>$idnumber))) {
894 return false;
897 if ($DB->record_exists('grade_items', array('courseid'=>$courseid, 'idnumber'=>$idnumber))) {
898 return false;
901 return true;
905 * Force final grade recalculation in all course items
907 * @param int $courseid The course ID to recalculate
909 function grade_force_full_regrading($courseid) {
910 global $DB;
911 $DB->set_field('grade_items', 'needsupdate', 1, array('courseid'=>$courseid));
915 * Forces regrading of all site grades. Used when changing site setings
917 function grade_force_site_regrading() {
918 global $CFG, $DB;
919 $DB->set_field('grade_items', 'needsupdate', 1);
923 * Recover a user's grades from grade_grades_history
924 * @param int $userid the user ID whose grades we want to recover
925 * @param int $courseid the relevant course
926 * @return bool true if successful or false if there was an error or no grades could be recovered
928 function grade_recover_history_grades($userid, $courseid) {
929 global $CFG, $DB;
931 if ($CFG->disablegradehistory) {
932 debugging('Attempting to recover grades when grade history is disabled.');
933 return false;
936 //Were grades recovered? Flag to return.
937 $recoveredgrades = false;
939 //Check the user is enrolled in this course
940 //Dont bother checking if they have a gradeable role. They may get one later so recover
941 //whatever grades they have now just in case.
942 $course_context = get_context_instance(CONTEXT_COURSE, $courseid);
943 if (!is_enrolled($course_context, $userid)) {
944 debugging('Attempting to recover the grades of a user who is deleted or not enrolled. Skipping recover.');
945 return false;
948 //Check for existing grades for this user in this course
949 //Recovering grades when the user already has grades can lead to duplicate indexes and bad data
950 //In the future we could move the existing grades to the history table then recover the grades from before then
951 $sql = "SELECT gg.id
952 FROM {grade_grades} gg
953 JOIN {grade_items} gi ON gi.id = gg.itemid
954 WHERE gi.courseid = :courseid AND gg.userid = :userid";
955 $params = array('userid' => $userid, 'courseid' => $courseid);
956 if ($DB->record_exists_sql($sql, $params)) {
957 debugging('Attempting to recover the grades of a user who already has grades. Skipping recover.');
958 return false;
959 } else {
960 //Retrieve the user's old grades
961 //have history ID as first column to guarantee we a unique first column
962 $sql = "SELECT h.id, gi.itemtype, gi.itemmodule, gi.iteminstance as iteminstance, gi.itemnumber, h.source, h.itemid, h.userid, h.rawgrade, h.rawgrademax,
963 h.rawgrademin, h.rawscaleid, h.usermodified, h.finalgrade, h.hidden, h.locked, h.locktime, h.exported, h.overridden, h.excluded, h.feedback,
964 h.feedbackformat, h.information, h.informationformat, h.timemodified, itemcreated.tm AS timecreated
965 FROM {grade_grades_history} h
966 JOIN (SELECT itemid, MAX(id) AS id
967 FROM {grade_grades_history}
968 WHERE userid = :userid1
969 GROUP BY itemid) maxquery ON h.id = maxquery.id AND h.itemid = maxquery.itemid
970 JOIN {grade_items} gi ON gi.id = h.itemid
971 JOIN (SELECT itemid, MAX(timemodified) AS tm
972 FROM {grade_grades_history}
973 WHERE userid = :userid2 AND action = :insertaction
974 GROUP BY itemid) itemcreated ON itemcreated.itemid = h.itemid
975 WHERE gi.courseid = :courseid";
976 $params = array('userid1' => $userid, 'userid2' => $userid , 'insertaction' => GRADE_HISTORY_INSERT, 'courseid' => $courseid);
977 $oldgrades = $DB->get_records_sql($sql, $params);
979 //now move the old grades to the grade_grades table
980 foreach ($oldgrades as $oldgrade) {
981 unset($oldgrade->id);
983 $grade = new grade_grade($oldgrade, false);//2nd arg false as dont want to try and retrieve a record from the DB
984 $grade->insert($oldgrade->source);
986 //dont include default empty grades created when activities are created
987 if (!is_null($oldgrade->finalgrade) || !is_null($oldgrade->feedback)) {
988 $recoveredgrades = true;
993 //Some activities require manual grade synching (moving grades from the activity into the gradebook)
994 //If the student was deleted when synching was done they may have grades in the activity that haven't been moved across
995 grade_grab_course_grades($courseid, null, $userid);
997 return $recoveredgrades;
1001 * Updates all final grades in course.
1003 * @param int $courseid The course ID
1004 * @param int $userid If specified try to do a quick regrading of the grades of this user only
1005 * @param object $updated_item Optional grade item to be marked for regrading
1006 * @return bool true if ok, array of errors if problems found. Grade item id => error message
1008 function grade_regrade_final_grades($courseid, $userid=null, $updated_item=null) {
1010 $course_item = grade_item::fetch_course_item($courseid);
1012 if ($userid) {
1013 // one raw grade updated for one user
1014 if (empty($updated_item)) {
1015 print_error("cannotbenull", 'debug', '', "updated_item");
1017 if ($course_item->needsupdate) {
1018 $updated_item->force_regrading();
1019 return array($course_item->id =>'Can not do fast regrading after updating of raw grades');
1022 } else {
1023 if (!$course_item->needsupdate) {
1024 // nothing to do :-)
1025 return true;
1029 $grade_items = grade_item::fetch_all(array('courseid'=>$courseid));
1030 $depends_on = array();
1032 // first mark all category and calculated items as needing regrading
1033 // this is slower, but 100% accurate
1034 foreach ($grade_items as $gid=>$gitem) {
1035 if (!empty($updated_item) and $updated_item->id == $gid) {
1036 $grade_items[$gid]->needsupdate = 1;
1038 } else if ($gitem->is_course_item() or $gitem->is_category_item() or $gitem->is_calculated()) {
1039 $grade_items[$gid]->needsupdate = 1;
1042 // construct depends_on lookup array
1043 $depends_on[$gid] = $grade_items[$gid]->depends_on();
1046 $errors = array();
1047 $finalids = array();
1048 $gids = array_keys($grade_items);
1049 $failed = 0;
1051 while (count($finalids) < count($gids)) { // work until all grades are final or error found
1052 $count = 0;
1053 foreach ($gids as $gid) {
1054 if (in_array($gid, $finalids)) {
1055 continue; // already final
1058 if (!$grade_items[$gid]->needsupdate) {
1059 $finalids[] = $gid; // we can make it final - does not need update
1060 continue;
1063 $doupdate = true;
1064 foreach ($depends_on[$gid] as $did) {
1065 if (!in_array($did, $finalids)) {
1066 $doupdate = false;
1067 continue; // this item depends on something that is not yet in finals array
1071 //oki - let's update, calculate or aggregate :-)
1072 if ($doupdate) {
1073 $result = $grade_items[$gid]->regrade_final_grades($userid);
1075 if ($result === true) {
1076 $grade_items[$gid]->regrading_finished();
1077 $grade_items[$gid]->check_locktime(); // do the locktime item locking
1078 $count++;
1079 $finalids[] = $gid;
1081 } else {
1082 $grade_items[$gid]->force_regrading();
1083 $errors[$gid] = $result;
1088 if ($count == 0) {
1089 $failed++;
1090 } else {
1091 $failed = 0;
1094 if ($failed > 1) {
1095 foreach($gids as $gid) {
1096 if (in_array($gid, $finalids)) {
1097 continue; // this one is ok
1099 $grade_items[$gid]->force_regrading();
1100 $errors[$grade_items[$gid]->id] = 'Probably circular reference or broken calculation formula'; // TODO: localize
1102 break; // oki, found error
1106 if (count($errors) == 0) {
1107 if (empty($userid)) {
1108 // do the locktime locking of grades, but only when doing full regrading
1109 grade_grade::check_locktime_all($gids);
1111 return true;
1112 } else {
1113 return $errors;
1118 * Refetches grade data from course activities
1120 * @param int $courseid The course ID
1121 * @param string $modname Limit the grade fetch to a single module type. For example 'forum'
1122 * @param int $userid limit the grade fetch to a single user
1124 function grade_grab_course_grades($courseid, $modname=null, $userid=0) {
1125 global $CFG, $DB;
1127 if ($modname) {
1128 $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname
1129 FROM {".$modname."} a, {course_modules} cm, {modules} m
1130 WHERE m.name=:modname AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=:courseid";
1131 $params = array('modname'=>$modname, 'courseid'=>$courseid);
1133 if ($modinstances = $DB->get_records_sql($sql, $params)) {
1134 foreach ($modinstances as $modinstance) {
1135 grade_update_mod_grades($modinstance, $userid);
1138 return;
1141 if (!$mods = get_plugin_list('mod') ) {
1142 print_error('nomodules', 'debug');
1145 foreach ($mods as $mod => $fullmod) {
1146 if ($mod == 'NEWMODULE') { // Someone has unzipped the template, ignore it
1147 continue;
1150 // include the module lib once
1151 if (file_exists($fullmod.'/lib.php')) {
1152 // get all instance of the activity
1153 $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname
1154 FROM {".$mod."} a, {course_modules} cm, {modules} m
1155 WHERE m.name=:mod AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=:courseid";
1156 $params = array('mod'=>$mod, 'courseid'=>$courseid);
1158 if ($modinstances = $DB->get_records_sql($sql, $params)) {
1159 foreach ($modinstances as $modinstance) {
1160 grade_update_mod_grades($modinstance, $userid);
1168 * Force full update of module grades in central gradebook
1170 * @param object $modinstance Module object with extra cmidnumber and modname property
1171 * @param int $userid Optional user ID if limiting the update to a single user
1172 * @return bool True if success
1174 function grade_update_mod_grades($modinstance, $userid=0) {
1175 global $CFG, $DB;
1177 $fullmod = $CFG->dirroot.'/mod/'.$modinstance->modname;
1178 if (!file_exists($fullmod.'/lib.php')) {
1179 debugging('missing lib.php file in module ' . $modinstance->modname);
1180 return false;
1182 include_once($fullmod.'/lib.php');
1184 $updateitemfunc = $modinstance->modname.'_grade_item_update';
1185 $updategradesfunc = $modinstance->modname.'_update_grades';
1187 if (function_exists($updategradesfunc) and function_exists($updateitemfunc)) {
1188 //new grading supported, force updating of grades
1189 $updateitemfunc($modinstance);
1190 $updategradesfunc($modinstance, $userid);
1192 } else {
1193 // mudule does not support grading??
1196 return true;
1200 * Remove grade letters for given context
1202 * @param context $context The context
1203 * @param bool $showfeedback If true a success notification will be displayed
1205 function remove_grade_letters($context, $showfeedback) {
1206 global $DB, $OUTPUT;
1208 $strdeleted = get_string('deleted');
1210 $DB->delete_records('grade_letters', array('contextid'=>$context->id));
1211 if ($showfeedback) {
1212 echo $OUTPUT->notification($strdeleted.' - '.get_string('letters', 'grades'), 'notifysuccess');
1217 * Remove all grade related course data
1218 * Grade history is kept
1220 * @param int $courseid The course ID
1221 * @param bool $showfeedback If true success notifications will be displayed
1223 function remove_course_grades($courseid, $showfeedback) {
1224 global $DB, $OUTPUT;
1226 $fs = get_file_storage();
1227 $strdeleted = get_string('deleted');
1229 $course_category = grade_category::fetch_course_category($courseid);
1230 $course_category->delete('coursedelete');
1231 $fs->delete_area_files(get_context_instance(CONTEXT_COURSE, $courseid)->id, 'grade', 'feedback');
1232 if ($showfeedback) {
1233 echo $OUTPUT->notification($strdeleted.' - '.get_string('grades', 'grades').', '.get_string('items', 'grades').', '.get_string('categories', 'grades'), 'notifysuccess');
1236 if ($outcomes = grade_outcome::fetch_all(array('courseid'=>$courseid))) {
1237 foreach ($outcomes as $outcome) {
1238 $outcome->delete('coursedelete');
1241 $DB->delete_records('grade_outcomes_courses', array('courseid'=>$courseid));
1242 if ($showfeedback) {
1243 echo $OUTPUT->notification($strdeleted.' - '.get_string('outcomes', 'grades'), 'notifysuccess');
1246 if ($scales = grade_scale::fetch_all(array('courseid'=>$courseid))) {
1247 foreach ($scales as $scale) {
1248 $scale->delete('coursedelete');
1251 if ($showfeedback) {
1252 echo $OUTPUT->notification($strdeleted.' - '.get_string('scales'), 'notifysuccess');
1255 $DB->delete_records('grade_settings', array('courseid'=>$courseid));
1256 if ($showfeedback) {
1257 echo $OUTPUT->notification($strdeleted.' - '.get_string('settings', 'grades'), 'notifysuccess');
1262 * Called when course category is deleted
1263 * Cleans the gradebook of associated data
1265 * @param int $categoryid The course category id
1266 * @param int $newparentid If empty everything is deleted. Otherwise the ID of the category where content moved
1267 * @param bool $showfeedback print feedback
1269 function grade_course_category_delete($categoryid, $newparentid, $showfeedback) {
1270 global $DB;
1272 $context = get_context_instance(CONTEXT_COURSECAT, $categoryid);
1273 $DB->delete_records('grade_letters', array('contextid'=>$context->id));
1277 * Does gradebook cleanup when a module is uninstalled
1278 * Deletes all associated grade items
1280 * @param string $modname The grade item module name to remove. For example 'forum'
1282 function grade_uninstalled_module($modname) {
1283 global $CFG, $DB;
1285 $sql = "SELECT *
1286 FROM {grade_items}
1287 WHERE itemtype='mod' AND itemmodule=?";
1289 // go all items for this module and delete them including the grades
1290 $rs = $DB->get_recordset_sql($sql, array($modname));
1291 foreach ($rs as $item) {
1292 $grade_item = new grade_item($item, false);
1293 $grade_item->delete('moduninstall');
1295 $rs->close();
1299 * Deletes all of a user's grade data from gradebook
1301 * @param int $userid The user whose grade data should be deleted
1303 function grade_user_delete($userid) {
1304 if ($grades = grade_grade::fetch_all(array('userid'=>$userid))) {
1305 foreach ($grades as $grade) {
1306 $grade->delete('userdelete');
1312 * Purge course data when user unenrolls from a course
1314 * @param int $courseid The ID of the course the user has unenrolled from
1315 * @param int $userid The ID of the user unenrolling
1317 function grade_user_unenrol($courseid, $userid) {
1318 if ($items = grade_item::fetch_all(array('courseid'=>$courseid))) {
1319 foreach ($items as $item) {
1320 if ($grades = grade_grade::fetch_all(array('userid'=>$userid, 'itemid'=>$item->id))) {
1321 foreach ($grades as $grade) {
1322 $grade->delete('userdelete');
1330 * Grading cron job. Performs background clean up on the gradebook
1332 function grade_cron() {
1333 global $CFG, $DB;
1335 $now = time();
1337 $sql = "SELECT i.*
1338 FROM {grade_items} i
1339 WHERE i.locked = 0 AND i.locktime > 0 AND i.locktime < ? AND EXISTS (
1340 SELECT 'x' FROM {grade_items} c WHERE c.itemtype='course' AND c.needsupdate=0 AND c.courseid=i.courseid)";
1342 // go through all courses that have proper final grades and lock them if needed
1343 $rs = $DB->get_recordset_sql($sql, array($now));
1344 foreach ($rs as $item) {
1345 $grade_item = new grade_item($item, false);
1346 $grade_item->locked = $now;
1347 $grade_item->update('locktime');
1349 $rs->close();
1351 $grade_inst = new grade_grade();
1352 $fields = 'g.'.implode(',g.', $grade_inst->required_fields);
1354 $sql = "SELECT $fields
1355 FROM {grade_grades} g, {grade_items} i
1356 WHERE g.locked = 0 AND g.locktime > 0 AND g.locktime < ? AND g.itemid=i.id AND EXISTS (
1357 SELECT 'x' FROM {grade_items} c WHERE c.itemtype='course' AND c.needsupdate=0 AND c.courseid=i.courseid)";
1359 // go through all courses that have proper final grades and lock them if needed
1360 $rs = $DB->get_recordset_sql($sql, array($now));
1361 foreach ($rs as $grade) {
1362 $grade_grade = new grade_grade($grade, false);
1363 $grade_grade->locked = $now;
1364 $grade_grade->update('locktime');
1366 $rs->close();
1368 //TODO: do not run this cleanup every cron invocation
1369 // cleanup history tables
1370 if (!empty($CFG->gradehistorylifetime)) { // value in days
1371 $histlifetime = $now - ($CFG->gradehistorylifetime * 3600 * 24);
1372 $tables = array('grade_outcomes_history', 'grade_categories_history', 'grade_items_history', 'grade_grades_history', 'scale_history');
1373 foreach ($tables as $table) {
1374 if ($DB->delete_records_select($table, "timemodified < ?", array($histlifetime))) {
1375 mtrace(" Deleted old grade history records from '$table'");
1382 * Reset all course grades, refetch from the activities and recalculate
1384 * @param int $courseid The course to reset
1385 * @return bool success
1387 function grade_course_reset($courseid) {
1389 // no recalculations
1390 grade_force_full_regrading($courseid);
1392 $grade_items = grade_item::fetch_all(array('courseid'=>$courseid));
1393 foreach ($grade_items as $gid=>$grade_item) {
1394 $grade_item->delete_all_grades('reset');
1397 //refetch all grades
1398 grade_grab_course_grades($courseid);
1400 // recalculate all grades
1401 grade_regrade_final_grades($courseid);
1402 return true;
1406 * Convert a number to 5 decimal point float, an empty string or a null db compatible format
1407 * (we need this to decide if db value changed)
1409 * @param mixed $number The number to convert
1410 * @return mixed float or null
1412 function grade_floatval($number) {
1413 if (is_null($number) or $number === '') {
1414 return null;
1416 // we must round to 5 digits to get the same precision as in 10,5 db fields
1417 // note: db rounding for 10,5 is different from php round() function
1418 return round($number, 5);
1422 * Compare two float numbers safely. Uses 5 decimals php precision using {@link grade_floatval()}. Nulls accepted too.
1423 * Used for determining if a database update is required
1425 * @param float $f1 Float one to compare
1426 * @param float $f2 Float two to compare
1427 * @return bool True if the supplied values are different
1429 function grade_floats_different($f1, $f2) {
1430 // note: db rounding for 10,5 is different from php round() function
1431 return (grade_floatval($f1) !== grade_floatval($f2));
1435 * Compare two float numbers safely. Uses 5 decimals php precision using {@link grade_floatval()}
1437 * Do not use rounding for 10,5 at the database level as the results may be
1438 * different from php round() function.
1440 * @since 2.0
1441 * @param float $f1 Float one to compare
1442 * @param float $f2 Float two to compare
1443 * @return bool True if the values should be considered as the same grades
1445 function grade_floats_equal($f1, $f2) {
1446 return (grade_floatval($f1) === grade_floatval($f2));