2 // This file is part of Moodle - http://moodle.org/
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
18 * Library of functions for gradebook - both public and internal
20 * @package core_grades
21 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 defined('MOODLE_INTERNAL') ||
die();
27 /** Include essential files */
28 require_once($CFG->libdir
. '/grade/constants.php');
30 require_once($CFG->libdir
. '/grade/grade_category.php');
31 require_once($CFG->libdir
. '/grade/grade_item.php');
32 require_once($CFG->libdir
. '/grade/grade_grade.php');
33 require_once($CFG->libdir
. '/grade/grade_scale.php');
34 require_once($CFG->libdir
. '/grade/grade_outcome.php');
36 /////////////////////////////////////////////////////////////////////
37 ///// Start of public API for communication with modules/blocks /////
38 /////////////////////////////////////////////////////////////////////
41 * Submit new or update grade; update/create grade_item definition. Grade must have userid specified,
42 * rawgrade and feedback with format are optional. rawgrade NULL means 'Not graded'.
43 * Missing property or key means does not change the existing value.
45 * Only following grade item properties can be changed 'itemname', 'idnumber', 'gradetype', 'grademax',
46 * 'grademin', 'scaleid', 'multfactor', 'plusfactor', 'deleted' and 'hidden'. 'reset' means delete all current grades including locked ones.
48 * Manual, course or category items can not be updated by this function.
51 * @param string $source Source of the grade such as 'mod/assignment'
52 * @param int $courseid ID of course
53 * @param string $itemtype Type of grade item. For example, mod or block
54 * @param string $itemmodule More specific then $itemtype. For example, assignment or forum. May be NULL for some item types
55 * @param int $iteminstance Instance ID of graded item
56 * @param int $itemnumber Most probably 0. Modules can use other numbers when having more than one grade for each user
57 * @param mixed $grades Grade (object, array) or several grades (arrays of arrays or objects), NULL if updating grade_item definition only
58 * @param mixed $itemdetails Object or array describing the grading item, NULL if no change
59 * @return int Returns GRADE_UPDATE_OK, GRADE_UPDATE_FAILED, GRADE_UPDATE_MULTIPLE or GRADE_UPDATE_ITEM_LOCKED
61 function grade_update($source, $courseid, $itemtype, $itemmodule, $iteminstance, $itemnumber, $grades=NULL, $itemdetails=NULL) {
62 global $USER, $CFG, $DB;
64 // only following grade_item properties can be changed in this function
65 $allowed = array('itemname', 'idnumber', 'gradetype', 'grademax', 'grademin', 'scaleid', 'multfactor', 'plusfactor', 'deleted', 'hidden');
66 // list of 10,5 numeric fields
67 $floats = array('grademin', 'grademax', 'multfactor', 'plusfactor');
69 // grade item identification
70 $params = compact('courseid', 'itemtype', 'itemmodule', 'iteminstance', 'itemnumber');
72 if (is_null($courseid) or is_null($itemtype)) {
73 debugging('Missing courseid or itemtype');
74 return GRADE_UPDATE_FAILED
;
77 if (!$grade_items = grade_item
::fetch_all($params)) {
81 } else if (count($grade_items) == 1){
82 $grade_item = reset($grade_items);
83 unset($grade_items); //release memory
86 debugging('Found more than one grade item');
87 return GRADE_UPDATE_MULTIPLE
;
90 if (!empty($itemdetails['deleted'])) {
92 if ($grade_item->delete($source)) {
93 return GRADE_UPDATE_OK
;
95 return GRADE_UPDATE_FAILED
;
98 return GRADE_UPDATE_OK
;
101 /// Create or update the grade_item if needed
105 $itemdetails = (array)$itemdetails;
107 // grademin and grademax ignored when scale specified
108 if (array_key_exists('scaleid', $itemdetails)) {
109 if ($itemdetails['scaleid']) {
110 unset($itemdetails['grademin']);
111 unset($itemdetails['grademax']);
115 foreach ($itemdetails as $k=>$v) {
116 if (!in_array($k, $allowed)) {
120 if ($k == 'gradetype' and $v == GRADE_TYPE_NONE
) {
121 // no grade item needed!
122 return GRADE_UPDATE_OK
;
127 $grade_item = new grade_item($params);
128 $grade_item->insert();
131 if ($grade_item->is_locked()) {
132 // no notice() here, test returned value instead!
133 return GRADE_UPDATE_ITEM_LOCKED
;
137 $itemdetails = (array)$itemdetails;
139 foreach ($itemdetails as $k=>$v) {
140 if (!in_array($k, $allowed)) {
144 if (in_array($k, $floats)) {
145 if (grade_floats_different($grade_item->{$k}, $v)) {
146 $grade_item->{$k} = $v;
151 if ($grade_item->{$k} != $v) {
152 $grade_item->{$k} = $v;
158 $grade_item->update();
163 /// reset grades if requested
164 if (!empty($itemdetails['reset'])) {
165 $grade_item->delete_all_grades('reset');
166 return GRADE_UPDATE_OK
;
169 /// Some extra checks
170 // do we use grading?
171 if ($grade_item->gradetype
== GRADE_TYPE_NONE
) {
172 return GRADE_UPDATE_OK
;
175 // no grade submitted
176 if (empty($grades)) {
177 return GRADE_UPDATE_OK
;
180 /// Finally start processing of grades
181 if (is_object($grades)) {
182 $grades = array($grades->userid
=>$grades);
184 if (array_key_exists('userid', $grades)) {
185 $grades = array($grades['userid']=>$grades);
189 /// normalize and verify grade array
190 foreach($grades as $k=>$g) {
196 if (empty($g['userid']) or $k != $g['userid']) {
197 debugging('Incorrect grade array index, must be user id! Grade ignored.');
202 if (empty($grades)) {
203 return GRADE_UPDATE_FAILED
;
206 $count = count($grades);
207 if ($count > 0 and $count < 200) {
208 list($uids, $params) = $DB->get_in_or_equal(array_keys($grades), SQL_PARAMS_NAMED
, $start='uid');
209 $params['gid'] = $grade_item->id
;
210 $sql = "SELECT * FROM {grade_grades} WHERE itemid = :gid AND userid $uids";
213 $sql = "SELECT * FROM {grade_grades} WHERE itemid = :gid";
214 $params = array('gid'=>$grade_item->id
);
217 $rs = $DB->get_recordset_sql($sql, $params);
221 while (count($grades) > 0) {
225 foreach ($rs as $gd) {
227 $userid = $gd->userid
;
228 if (!isset($grades[$userid])) {
229 // this grade not requested, continue
232 // existing grade requested
233 $grade = $grades[$userid];
234 $grade_grade = new grade_grade($gd, false);
235 unset($grades[$userid]);
239 if (is_null($grade_grade)) {
240 if (count($grades) == 0) {
241 // no more grades to process
245 $grade = reset($grades);
246 $userid = $grade['userid'];
247 $grade_grade = new grade_grade(array('itemid'=>$grade_item->id
, 'userid'=>$userid), false);
248 $grade_grade->load_optional_fields(); // add feedback and info too
249 unset($grades[$userid]);
254 $feedbackformat = FORMAT_MOODLE
;
255 $usermodified = $USER->id
;
256 $datesubmitted = null;
259 if (array_key_exists('rawgrade', $grade)) {
260 $rawgrade = $grade['rawgrade'];
263 if (array_key_exists('feedback', $grade)) {
264 $feedback = $grade['feedback'];
267 if (array_key_exists('feedbackformat', $grade)) {
268 $feedbackformat = $grade['feedbackformat'];
271 if (array_key_exists('usermodified', $grade)) {
272 $usermodified = $grade['usermodified'];
275 if (array_key_exists('datesubmitted', $grade)) {
276 $datesubmitted = $grade['datesubmitted'];
279 if (array_key_exists('dategraded', $grade)) {
280 $dategraded = $grade['dategraded'];
283 // update or insert the grade
284 if (!$grade_item->update_raw_grade($userid, $rawgrade, $source, $feedback, $feedbackformat, $usermodified, $dategraded, $datesubmitted, $grade_grade)) {
294 return GRADE_UPDATE_OK
;
296 return GRADE_UPDATE_FAILED
;
301 * Updates a user's outcomes. Manual outcomes can not be updated.
304 * @param string $source Source of the grade such as 'mod/assignment'
305 * @param int $courseid ID of course
306 * @param string $itemtype Type of grade item. For example, 'mod' or 'block'
307 * @param string $itemmodule More specific then $itemtype. For example, 'forum' or 'quiz'. May be NULL for some item types
308 * @param int $iteminstance Instance ID of graded item. For example the forum ID.
309 * @param int $userid ID of the graded user
310 * @param array $data Array consisting of grade item itemnumber ({@link grade_update()}) => outcomegrade
311 * @return bool returns true if grade items were found and updated successfully
313 function grade_update_outcomes($source, $courseid, $itemtype, $itemmodule, $iteminstance, $userid, $data) {
314 if ($items = grade_item
::fetch_all(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid))) {
316 foreach ($items as $item) {
317 if (!array_key_exists($item->itemnumber
, $data)) {
320 $grade = $data[$item->itemnumber
] < 1 ?
null : $data[$item->itemnumber
];
321 $result = ($item->update_final_grade($userid, $grade, $source) && $result);
325 return false; //grade items not found
329 * Returns grading information for one or more activities, optionally with user grades
330 * Manual, course or category items can not be queried.
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 = null, $itemmodule = null, $iteminstance = null, $userid_or_ids=null) {
343 if (empty($itemtype) or empty($itemmodule) or empty($iteminstance)) {
344 debugging('itemtype, itemmodule or iteminstance parameters should not be empty. For Moodle 2.8 and onwards are required',
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 $params = array('courseid' => $courseid);
362 if (!empty($itemtype)) {
363 $params['itemtype'] = $itemtype;
365 if (!empty($itemmodule)) {
366 $params['itemmodule'] = $itemmodule;
368 if (!empty($iteminstance)) {
369 $params['iteminstance'] = $iteminstance;
371 if ($grade_items = grade_item
::fetch_all($params)) {
372 foreach ($grade_items as $grade_item) {
373 $decimalpoints = null;
375 if (empty($grade_item->outcomeid
)) {
376 // prepare information about grade item
377 $item = new stdClass();
378 $item->id
= $grade_item->id
;
379 $item->itemnumber
= $grade_item->itemnumber
;
380 $item->itemtype
= $grade_item->itemtype
;
381 $item->itemmodule
= $grade_item->itemmodule
;
382 $item->iteminstance
= $grade_item->iteminstance
;
383 $item->scaleid
= $grade_item->scaleid
;
384 $item->name
= $grade_item->get_name();
385 $item->grademin
= $grade_item->grademin
;
386 $item->grademax
= $grade_item->grademax
;
387 $item->gradepass
= $grade_item->gradepass
;
388 $item->locked
= $grade_item->is_locked();
389 $item->hidden
= $grade_item->is_hidden();
390 $item->grades
= array();
392 switch ($grade_item->gradetype
) {
393 case GRADE_TYPE_NONE
:
396 case GRADE_TYPE_VALUE
:
400 case GRADE_TYPE_TEXT
:
404 $item->gradepass
= 0;
408 if (empty($userid_or_ids)) {
411 } else if (is_array($userid_or_ids)) {
412 $userids = $userid_or_ids;
415 $userids = array($userid_or_ids);
419 $grade_grades = grade_grade
::fetch_users_grades($grade_item, $userids, true);
420 foreach ($userids as $userid) {
421 $grade_grades[$userid]->grade_item
=& $grade_item;
423 $grade = new stdClass();
424 $grade->grade
= $grade_grades[$userid]->finalgrade
;
425 $grade->locked
= $grade_grades[$userid]->is_locked();
426 $grade->hidden
= $grade_grades[$userid]->is_hidden();
427 $grade->overridden
= $grade_grades[$userid]->overridden
;
428 $grade->feedback
= $grade_grades[$userid]->feedback
;
429 $grade->feedbackformat
= $grade_grades[$userid]->feedbackformat
;
430 $grade->usermodified
= $grade_grades[$userid]->usermodified
;
431 $grade->datesubmitted
= $grade_grades[$userid]->get_datesubmitted();
432 $grade->dategraded
= $grade_grades[$userid]->get_dategraded();
434 // create text representation of grade
435 if ($grade_item->gradetype
== GRADE_TYPE_TEXT
or $grade_item->gradetype
== GRADE_TYPE_NONE
) {
436 $grade->grade
= null;
437 $grade->str_grade
= '-';
438 $grade->str_long_grade
= $grade->str_grade
;
440 } else if (in_array($grade_item->id
, $needsupdate)) {
441 $grade->grade
= false;
442 $grade->str_grade
= get_string('error');
443 $grade->str_long_grade
= $grade->str_grade
;
445 } else if (is_null($grade->grade
)) {
446 $grade->str_grade
= '-';
447 $grade->str_long_grade
= $grade->str_grade
;
450 $grade->str_grade
= grade_format_gradevalue($grade->grade
, $grade_item);
451 if ($grade_item->gradetype
== GRADE_TYPE_SCALE
or $grade_item->get_displaytype() != GRADE_DISPLAY_TYPE_REAL
) {
452 $grade->str_long_grade
= $grade->str_grade
;
455 $a->grade
= $grade->str_grade
;
456 $a->max
= grade_format_gradevalue($grade_item->grademax
, $grade_item);
457 $grade->str_long_grade
= get_string('gradelong', 'grades', $a);
461 // create html representation of feedback
462 if (is_null($grade->feedback
)) {
463 $grade->str_feedback
= '';
465 $grade->str_feedback
= format_text($grade->feedback
, $grade->feedbackformat
);
468 $item->grades
[$userid] = $grade;
471 $return->items
[$grade_item->itemnumber
] = $item;
474 if (!$grade_outcome = grade_outcome
::fetch(array('id'=>$grade_item->outcomeid
))) {
475 debugging('Incorect outcomeid found');
480 $outcome = new stdClass();
481 $outcome->id
= $grade_item->id
;
482 $outcome->itemnumber
= $grade_item->itemnumber
;
483 $outcome->itemtype
= $grade_item->itemtype
;
484 $outcome->itemmodule
= $grade_item->itemmodule
;
485 $outcome->iteminstance
= $grade_item->iteminstance
;
486 $outcome->scaleid
= $grade_outcome->scaleid
;
487 $outcome->name
= $grade_outcome->get_name();
488 $outcome->locked
= $grade_item->is_locked();
489 $outcome->hidden
= $grade_item->is_hidden();
491 if (empty($userid_or_ids)) {
493 } else if (is_array($userid_or_ids)) {
494 $userids = $userid_or_ids;
496 $userids = array($userid_or_ids);
500 $grade_grades = grade_grade
::fetch_users_grades($grade_item, $userids, true);
501 foreach ($userids as $userid) {
502 $grade_grades[$userid]->grade_item
=& $grade_item;
504 $grade = new stdClass();
505 $grade->grade
= $grade_grades[$userid]->finalgrade
;
506 $grade->locked
= $grade_grades[$userid]->is_locked();
507 $grade->hidden
= $grade_grades[$userid]->is_hidden();
508 $grade->feedback
= $grade_grades[$userid]->feedback
;
509 $grade->feedbackformat
= $grade_grades[$userid]->feedbackformat
;
510 $grade->usermodified
= $grade_grades[$userid]->usermodified
;
512 // create text representation of grade
513 if (in_array($grade_item->id
, $needsupdate)) {
514 $grade->grade
= false;
515 $grade->str_grade
= get_string('error');
517 } else if (is_null($grade->grade
)) {
519 $grade->str_grade
= get_string('nooutcome', 'grades');
522 $grade->grade
= (int)$grade->grade
;
523 $scale = $grade_item->load_scale();
524 $grade->str_grade
= format_string($scale->scale_items
[(int)$grade->grade
-1]);
527 // create html representation of feedback
528 if (is_null($grade->feedback
)) {
529 $grade->str_feedback
= '';
531 $grade->str_feedback
= format_text($grade->feedback
, $grade->feedbackformat
);
534 $outcome->grades
[$userid] = $grade;
538 if (isset($return->outcomes
[$grade_item->itemnumber
])) {
539 // itemnumber duplicates - lets fix them!
540 $newnumber = $grade_item->itemnumber +
1;
541 while(grade_item
::fetch(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid, 'itemnumber'=>$newnumber))) {
544 $outcome->itemnumber
= $newnumber;
545 $grade_item->itemnumber
= $newnumber;
546 $grade_item->update('system');
549 $return->outcomes
[$grade_item->itemnumber
] = $outcome;
555 // sort results using itemnumbers
556 ksort($return->items
, SORT_NUMERIC
);
557 ksort($return->outcomes
, SORT_NUMERIC
);
562 ///////////////////////////////////////////////////////////////////
563 ///// End of public API for communication with modules/blocks /////
564 ///////////////////////////////////////////////////////////////////
568 ///////////////////////////////////////////////////////////////////
569 ///// Internal API: used by gradebook plugins and Moodle core /////
570 ///////////////////////////////////////////////////////////////////
573 * Returns a course gradebook setting
575 * @param int $courseid
576 * @param string $name of setting, maybe null if reset only
577 * @param string $default value to return if setting is not found
578 * @param bool $resetcache force reset of internal static cache
579 * @return string value of the setting, $default if setting not found, NULL if supplied $name is null
581 function grade_get_setting($courseid, $name, $default=null, $resetcache=false) {
584 static $cache = array();
586 if ($resetcache or !array_key_exists($courseid, $cache)) {
587 $cache[$courseid] = array();
589 } else if (is_null($name)) {
592 } else if (array_key_exists($name, $cache[$courseid])) {
593 return $cache[$courseid][$name];
596 if (!$data = $DB->get_record('grade_settings', array('courseid'=>$courseid, 'name'=>$name))) {
599 $result = $data->value
;
602 if (is_null($result)) {
606 $cache[$courseid][$name] = $result;
611 * Returns all course gradebook settings as object properties
613 * @param int $courseid
616 function grade_get_settings($courseid) {
619 $settings = new stdClass();
620 $settings->id
= $courseid;
622 if ($records = $DB->get_records('grade_settings', array('courseid'=>$courseid))) {
623 foreach ($records as $record) {
624 $settings->{$record->name
} = $record->value
;
632 * Add, update or delete a course gradebook setting
634 * @param int $courseid The course ID
635 * @param string $name Name of the setting
636 * @param string $value Value of the setting. NULL means delete the setting.
638 function grade_set_setting($courseid, $name, $value) {
641 if (is_null($value)) {
642 $DB->delete_records('grade_settings', array('courseid'=>$courseid, 'name'=>$name));
644 } else if (!$existing = $DB->get_record('grade_settings', array('courseid'=>$courseid, 'name'=>$name))) {
645 $data = new stdClass();
646 $data->courseid
= $courseid;
648 $data->value
= $value;
649 $DB->insert_record('grade_settings', $data);
652 $data = new stdClass();
653 $data->id
= $existing->id
;
654 $data->value
= $value;
655 $DB->update_record('grade_settings', $data);
658 grade_get_setting($courseid, null, null, true); // reset the cache
662 * Returns string representation of grade value
664 * @param float $value The grade value
665 * @param object $grade_item Grade item object passed by reference to prevent scale reloading
666 * @param bool $localized use localised decimal separator
667 * @param int $displaytype type of display. For example GRADE_DISPLAY_TYPE_REAL, GRADE_DISPLAY_TYPE_PERCENTAGE, GRADE_DISPLAY_TYPE_LETTER
668 * @param int $decimals The number of decimal places when displaying float values
671 function grade_format_gradevalue($value, &$grade_item, $localized=true, $displaytype=null, $decimals=null) {
672 if ($grade_item->gradetype
== GRADE_TYPE_NONE
or $grade_item->gradetype
== GRADE_TYPE_TEXT
) {
677 if (is_null($value)) {
681 if ($grade_item->gradetype
!= GRADE_TYPE_VALUE
and $grade_item->gradetype
!= GRADE_TYPE_SCALE
) {
686 if (is_null($displaytype)) {
687 $displaytype = $grade_item->get_displaytype();
690 if (is_null($decimals)) {
691 $decimals = $grade_item->get_decimals();
694 switch ($displaytype) {
695 case GRADE_DISPLAY_TYPE_REAL
:
696 return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized);
698 case GRADE_DISPLAY_TYPE_PERCENTAGE
:
699 return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized);
701 case GRADE_DISPLAY_TYPE_LETTER
:
702 return grade_format_gradevalue_letter($value, $grade_item);
704 case GRADE_DISPLAY_TYPE_REAL_PERCENTAGE
:
705 return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ' (' .
706 grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ')';
708 case GRADE_DISPLAY_TYPE_REAL_LETTER
:
709 return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ' (' .
710 grade_format_gradevalue_letter($value, $grade_item) . ')';
712 case GRADE_DISPLAY_TYPE_PERCENTAGE_REAL
:
713 return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ' (' .
714 grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ')';
716 case GRADE_DISPLAY_TYPE_LETTER_REAL
:
717 return grade_format_gradevalue_letter($value, $grade_item) . ' (' .
718 grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ')';
720 case GRADE_DISPLAY_TYPE_LETTER_PERCENTAGE
:
721 return grade_format_gradevalue_letter($value, $grade_item) . ' (' .
722 grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ')';
724 case GRADE_DISPLAY_TYPE_PERCENTAGE_LETTER
:
725 return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ' (' .
726 grade_format_gradevalue_letter($value, $grade_item) . ')';
733 * Returns a float 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
741 function grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) {
742 if ($grade_item->gradetype
== GRADE_TYPE_SCALE
) {
743 if (!$scale = $grade_item->load_scale()) {
744 return get_string('error');
747 $value = $grade_item->bounded_grade($value);
748 return format_string($scale->scale_items
[$value-1]);
751 return format_float($value, $decimals, $localized);
756 * Returns a percentage representation of a grade value
758 * @param float $value The grade value
759 * @param object $grade_item Grade item object
760 * @param int $decimals The number of decimal places
761 * @param bool $localized use localised decimal separator
764 function grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) {
765 $min = $grade_item->grademin
;
766 $max = $grade_item->grademax
;
770 $value = $grade_item->bounded_grade($value);
771 $percentage = (($value-$min)*100)/($max-$min);
772 return format_float($percentage, $decimals, $localized).' %';
776 * Returns a letter grade representation of a grade value
777 * The array of grade letters used is produced by {@link grade_get_letters()} using the course context
779 * @param float $value The grade value
780 * @param object $grade_item Grade item object
783 function grade_format_gradevalue_letter($value, $grade_item) {
784 $context = context_course
::instance($grade_item->courseid
, IGNORE_MISSING
);
785 if (!$letters = grade_get_letters($context)) {
786 return ''; // no letters??
789 if (is_null($value)) {
793 $value = grade_grade
::standardise_score($value, $grade_item->grademin
, $grade_item->grademax
, 0, 100);
794 $value = bounded_number(0, $value, 100); // just in case
795 foreach ($letters as $boundary => $letter) {
796 if ($value >= $boundary) {
797 return format_string($letter);
800 return '-'; // no match? maybe '' would be more correct
805 * Returns grade options for gradebook grade category menu
807 * @param int $courseid The course ID
808 * @param bool $includenew Include option for new category at array index -1
809 * @return array of grade categories in course
811 function grade_get_categories_menu($courseid, $includenew=false) {
813 if (!$categories = grade_category
::fetch_all(array('courseid'=>$courseid))) {
814 //make sure course category exists
815 if (!grade_category
::fetch_course_category($courseid)) {
816 debugging('Can not create course grade category!');
819 $categories = grade_category
::fetch_all(array('courseid'=>$courseid));
821 foreach ($categories as $key=>$category) {
822 if ($category->is_course_category()) {
823 $result[$category->id
] = get_string('uncategorised', 'grades');
824 unset($categories[$key]);
828 $result[-1] = get_string('newcategory', 'grades');
831 foreach ($categories as $category) {
832 $cats[$category->id
] = $category->get_name();
834 core_collator
::asort($cats);
836 return ($result+
$cats);
840 * Returns the array of grade letters to be used in the supplied context
842 * @param object $context Context object or null for defaults
843 * @return array of grade_boundary (minimum) => letter_string
845 function grade_get_letters($context=null) {
848 if (empty($context)) {
849 //default grading letters
850 return array('93'=>'A', '90'=>'A-', '87'=>'B+', '83'=>'B', '80'=>'B-', '77'=>'C+', '73'=>'C', '70'=>'C-', '67'=>'D+', '60'=>'D', '0'=>'F');
853 static $cache = array();
855 if (array_key_exists($context->id
, $cache)) {
856 return $cache[$context->id
];
859 if (count($cache) > 100) {
860 $cache = array(); // cache size limit
865 $contexts = $context->get_parent_context_ids();
866 array_unshift($contexts, $context->id
);
868 foreach ($contexts as $ctxid) {
869 if ($records = $DB->get_records('grade_letters', array('contextid'=>$ctxid), 'lowerboundary DESC')) {
870 foreach ($records as $record) {
871 $letters[$record->lowerboundary
] = $record->letter
;
875 if (!empty($letters)) {
876 $cache[$context->id
] = $letters;
881 $letters = grade_get_letters(null);
882 $cache[$context->id
] = $letters;
888 * Verify new value of grade item idnumber. Checks for uniqueness of new ID numbers. Old ID numbers are kept intact.
890 * @param string $idnumber string (with magic quotes)
891 * @param int $courseid ID numbers are course unique only
892 * @param grade_item $grade_item The grade item this idnumber is associated with
893 * @param stdClass $cm used for course module idnumbers and items attached to modules
894 * @return bool true means idnumber ok
896 function grade_verify_idnumber($idnumber, $courseid, $grade_item=null, $cm=null) {
899 if ($idnumber == '') {
900 //we allow empty idnumbers
904 // keep existing even when not unique
905 if ($cm and $cm->idnumber
== $idnumber) {
906 if ($grade_item and $grade_item->itemnumber
!= 0) {
907 // grade item with itemnumber > 0 can't have the same idnumber as the main
908 // itemnumber 0 which is synced with course_modules
912 } else if ($grade_item and $grade_item->idnumber
== $idnumber) {
916 if ($DB->record_exists('course_modules', array('course'=>$courseid, 'idnumber'=>$idnumber))) {
920 if ($DB->record_exists('grade_items', array('courseid'=>$courseid, 'idnumber'=>$idnumber))) {
928 * Force final grade recalculation in all course items
930 * @param int $courseid The course ID to recalculate
932 function grade_force_full_regrading($courseid) {
934 $DB->set_field('grade_items', 'needsupdate', 1, array('courseid'=>$courseid));
938 * Forces regrading of all site grades. Used when changing site setings
940 function grade_force_site_regrading() {
942 $DB->set_field('grade_items', 'needsupdate', 1);
946 * Recover a user's grades from grade_grades_history
947 * @param int $userid the user ID whose grades we want to recover
948 * @param int $courseid the relevant course
949 * @return bool true if successful or false if there was an error or no grades could be recovered
951 function grade_recover_history_grades($userid, $courseid) {
954 if ($CFG->disablegradehistory
) {
955 debugging('Attempting to recover grades when grade history is disabled.');
959 //Were grades recovered? Flag to return.
960 $recoveredgrades = false;
962 //Check the user is enrolled in this course
963 //Dont bother checking if they have a gradeable role. They may get one later so recover
964 //whatever grades they have now just in case.
965 $course_context = context_course
::instance($courseid);
966 if (!is_enrolled($course_context, $userid)) {
967 debugging('Attempting to recover the grades of a user who is deleted or not enrolled. Skipping recover.');
971 //Check for existing grades for this user in this course
972 //Recovering grades when the user already has grades can lead to duplicate indexes and bad data
973 //In the future we could move the existing grades to the history table then recover the grades from before then
975 FROM {grade_grades} gg
976 JOIN {grade_items} gi ON gi.id = gg.itemid
977 WHERE gi.courseid = :courseid AND gg.userid = :userid";
978 $params = array('userid' => $userid, 'courseid' => $courseid);
979 if ($DB->record_exists_sql($sql, $params)) {
980 debugging('Attempting to recover the grades of a user who already has grades. Skipping recover.');
983 //Retrieve the user's old grades
984 //have history ID as first column to guarantee we a unique first column
985 $sql = "SELECT h.id, gi.itemtype, gi.itemmodule, gi.iteminstance as iteminstance, gi.itemnumber, h.source, h.itemid, h.userid, h.rawgrade, h.rawgrademax,
986 h.rawgrademin, h.rawscaleid, h.usermodified, h.finalgrade, h.hidden, h.locked, h.locktime, h.exported, h.overridden, h.excluded, h.feedback,
987 h.feedbackformat, h.information, h.informationformat, h.timemodified, itemcreated.tm AS timecreated
988 FROM {grade_grades_history} h
989 JOIN (SELECT itemid, MAX(id) AS id
990 FROM {grade_grades_history}
991 WHERE userid = :userid1
992 GROUP BY itemid) maxquery ON h.id = maxquery.id AND h.itemid = maxquery.itemid
993 JOIN {grade_items} gi ON gi.id = h.itemid
994 JOIN (SELECT itemid, MAX(timemodified) AS tm
995 FROM {grade_grades_history}
996 WHERE userid = :userid2 AND action = :insertaction
997 GROUP BY itemid) itemcreated ON itemcreated.itemid = h.itemid
998 WHERE gi.courseid = :courseid";
999 $params = array('userid1' => $userid, 'userid2' => $userid , 'insertaction' => GRADE_HISTORY_INSERT
, 'courseid' => $courseid);
1000 $oldgrades = $DB->get_records_sql($sql, $params);
1002 //now move the old grades to the grade_grades table
1003 foreach ($oldgrades as $oldgrade) {
1004 unset($oldgrade->id
);
1006 $grade = new grade_grade($oldgrade, false);//2nd arg false as dont want to try and retrieve a record from the DB
1007 $grade->insert($oldgrade->source
);
1009 //dont include default empty grades created when activities are created
1010 if (!is_null($oldgrade->finalgrade
) ||
!is_null($oldgrade->feedback
)) {
1011 $recoveredgrades = true;
1016 //Some activities require manual grade synching (moving grades from the activity into the gradebook)
1017 //If the student was deleted when synching was done they may have grades in the activity that haven't been moved across
1018 grade_grab_course_grades($courseid, null, $userid);
1020 return $recoveredgrades;
1024 * Updates all final grades in course.
1026 * @param int $courseid The course ID
1027 * @param int $userid If specified try to do a quick regrading of the grades of this user only
1028 * @param object $updated_item Optional grade item to be marked for regrading
1029 * @return bool true if ok, array of errors if problems found. Grade item id => error message
1031 function grade_regrade_final_grades($courseid, $userid=null, $updated_item=null) {
1032 // This may take a very long time.
1033 \core_php_time_limit
::raise();
1035 $course_item = grade_item
::fetch_course_item($courseid);
1038 // one raw grade updated for one user
1039 if (empty($updated_item)) {
1040 print_error("cannotbenull", 'debug', '', "updated_item");
1042 if ($course_item->needsupdate
) {
1043 $updated_item->force_regrading();
1044 return array($course_item->id
=>'Can not do fast regrading after updating of raw grades');
1048 if (!$course_item->needsupdate
) {
1049 // nothing to do :-)
1054 $grade_items = grade_item
::fetch_all(array('courseid'=>$courseid));
1055 $depends_on = array();
1057 // first mark all category and calculated items as needing regrading
1058 // this is slower, but 100% accurate
1059 foreach ($grade_items as $gid=>$gitem) {
1060 if (!empty($updated_item) and $updated_item->id
== $gid) {
1061 $grade_items[$gid]->needsupdate
= 1;
1063 } else if ($gitem->is_course_item() or $gitem->is_category_item() or $gitem->is_calculated()) {
1064 $grade_items[$gid]->needsupdate
= 1;
1067 // construct depends_on lookup array
1068 $depends_on[$gid] = $grade_items[$gid]->depends_on();
1072 $finalids = array();
1073 $gids = array_keys($grade_items);
1076 while (count($finalids) < count($gids)) { // work until all grades are final or error found
1078 foreach ($gids as $gid) {
1079 if (in_array($gid, $finalids)) {
1080 continue; // already final
1083 if (!$grade_items[$gid]->needsupdate
) {
1084 $finalids[] = $gid; // we can make it final - does not need update
1089 foreach ($depends_on[$gid] as $did) {
1090 if (!in_array($did, $finalids)) {
1092 continue; // this item depends on something that is not yet in finals array
1096 //oki - let's update, calculate or aggregate :-)
1098 $result = $grade_items[$gid]->regrade_final_grades($userid);
1100 if ($result === true) {
1101 $grade_items[$gid]->regrading_finished();
1102 $grade_items[$gid]->check_locktime(); // do the locktime item locking
1107 $grade_items[$gid]->force_regrading();
1108 $errors[$gid] = $result;
1120 foreach($gids as $gid) {
1121 if (in_array($gid, $finalids)) {
1122 continue; // this one is ok
1124 $grade_items[$gid]->force_regrading();
1125 $errors[$grade_items[$gid]->id
] = get_string('errorcalculationbroken', 'grades');
1127 break; // Found error.
1131 if (count($errors) == 0) {
1132 if (empty($userid)) {
1133 // do the locktime locking of grades, but only when doing full regrading
1134 grade_grade
::check_locktime_all($gids);
1143 * Refetches grade data from course activities
1145 * @param int $courseid The course ID
1146 * @param string $modname Limit the grade fetch to a single module type. For example 'forum'
1147 * @param int $userid limit the grade fetch to a single user
1149 function grade_grab_course_grades($courseid, $modname=null, $userid=0) {
1153 $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname
1154 FROM {".$modname."} a, {course_modules} cm, {modules} m
1155 WHERE m.name=:modname AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=:courseid";
1156 $params = array('modname'=>$modname, 'courseid'=>$courseid);
1158 if ($modinstances = $DB->get_records_sql($sql, $params)) {
1159 foreach ($modinstances as $modinstance) {
1160 grade_update_mod_grades($modinstance, $userid);
1166 if (!$mods = core_component
::get_plugin_list('mod') ) {
1167 print_error('nomodules', 'debug');
1170 foreach ($mods as $mod => $fullmod) {
1171 if ($mod == 'NEWMODULE') { // Someone has unzipped the template, ignore it
1175 // include the module lib once
1176 if (file_exists($fullmod.'/lib.php')) {
1177 // get all instance of the activity
1178 $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname
1179 FROM {".$mod."} a, {course_modules} cm, {modules} m
1180 WHERE m.name=:mod AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=:courseid";
1181 $params = array('mod'=>$mod, 'courseid'=>$courseid);
1183 if ($modinstances = $DB->get_records_sql($sql, $params)) {
1184 foreach ($modinstances as $modinstance) {
1185 grade_update_mod_grades($modinstance, $userid);
1193 * Force full update of module grades in central gradebook
1195 * @param object $modinstance Module object with extra cmidnumber and modname property
1196 * @param int $userid Optional user ID if limiting the update to a single user
1197 * @return bool True if success
1199 function grade_update_mod_grades($modinstance, $userid=0) {
1202 $fullmod = $CFG->dirroot
.'/mod/'.$modinstance->modname
;
1203 if (!file_exists($fullmod.'/lib.php')) {
1204 debugging('missing lib.php file in module ' . $modinstance->modname
);
1207 include_once($fullmod.'/lib.php');
1209 $updateitemfunc = $modinstance->modname
.'_grade_item_update';
1210 $updategradesfunc = $modinstance->modname
.'_update_grades';
1212 if (function_exists($updategradesfunc) and function_exists($updateitemfunc)) {
1213 //new grading supported, force updating of grades
1214 $updateitemfunc($modinstance);
1215 $updategradesfunc($modinstance, $userid);
1218 // Module does not support grading?
1225 * Remove grade letters for given context
1227 * @param context $context The context
1228 * @param bool $showfeedback If true a success notification will be displayed
1230 function remove_grade_letters($context, $showfeedback) {
1231 global $DB, $OUTPUT;
1233 $strdeleted = get_string('deleted');
1235 $DB->delete_records('grade_letters', array('contextid'=>$context->id
));
1236 if ($showfeedback) {
1237 echo $OUTPUT->notification($strdeleted.' - '.get_string('letters', 'grades'), 'notifysuccess');
1242 * Remove all grade related course data
1243 * Grade history is kept
1245 * @param int $courseid The course ID
1246 * @param bool $showfeedback If true success notifications will be displayed
1248 function remove_course_grades($courseid, $showfeedback) {
1249 global $DB, $OUTPUT;
1251 $fs = get_file_storage();
1252 $strdeleted = get_string('deleted');
1254 $course_category = grade_category
::fetch_course_category($courseid);
1255 $course_category->delete('coursedelete');
1256 $fs->delete_area_files(context_course
::instance($courseid)->id
, 'grade', 'feedback');
1257 if ($showfeedback) {
1258 echo $OUTPUT->notification($strdeleted.' - '.get_string('grades', 'grades').', '.get_string('items', 'grades').', '.get_string('categories', 'grades'), 'notifysuccess');
1261 if ($outcomes = grade_outcome
::fetch_all(array('courseid'=>$courseid))) {
1262 foreach ($outcomes as $outcome) {
1263 $outcome->delete('coursedelete');
1266 $DB->delete_records('grade_outcomes_courses', array('courseid'=>$courseid));
1267 if ($showfeedback) {
1268 echo $OUTPUT->notification($strdeleted.' - '.get_string('outcomes', 'grades'), 'notifysuccess');
1271 if ($scales = grade_scale
::fetch_all(array('courseid'=>$courseid))) {
1272 foreach ($scales as $scale) {
1273 $scale->delete('coursedelete');
1276 if ($showfeedback) {
1277 echo $OUTPUT->notification($strdeleted.' - '.get_string('scales'), 'notifysuccess');
1280 $DB->delete_records('grade_settings', array('courseid'=>$courseid));
1281 if ($showfeedback) {
1282 echo $OUTPUT->notification($strdeleted.' - '.get_string('settings', 'grades'), 'notifysuccess');
1287 * Called when course category is deleted
1288 * Cleans the gradebook of associated data
1290 * @param int $categoryid The course category id
1291 * @param int $newparentid If empty everything is deleted. Otherwise the ID of the category where content moved
1292 * @param bool $showfeedback print feedback
1294 function grade_course_category_delete($categoryid, $newparentid, $showfeedback) {
1297 $context = context_coursecat
::instance($categoryid);
1298 $DB->delete_records('grade_letters', array('contextid'=>$context->id
));
1302 * Does gradebook cleanup when a module is uninstalled
1303 * Deletes all associated grade items
1305 * @param string $modname The grade item module name to remove. For example 'forum'
1307 function grade_uninstalled_module($modname) {
1312 WHERE itemtype='mod' AND itemmodule=?";
1314 // go all items for this module and delete them including the grades
1315 $rs = $DB->get_recordset_sql($sql, array($modname));
1316 foreach ($rs as $item) {
1317 $grade_item = new grade_item($item, false);
1318 $grade_item->delete('moduninstall');
1324 * Deletes all of a user's grade data from gradebook
1326 * @param int $userid The user whose grade data should be deleted
1328 function grade_user_delete($userid) {
1329 if ($grades = grade_grade
::fetch_all(array('userid'=>$userid))) {
1330 foreach ($grades as $grade) {
1331 $grade->delete('userdelete');
1337 * Purge course data when user unenrolls from a course
1339 * @param int $courseid The ID of the course the user has unenrolled from
1340 * @param int $userid The ID of the user unenrolling
1342 function grade_user_unenrol($courseid, $userid) {
1343 if ($items = grade_item
::fetch_all(array('courseid'=>$courseid))) {
1344 foreach ($items as $item) {
1345 if ($grades = grade_grade
::fetch_all(array('userid'=>$userid, 'itemid'=>$item->id
))) {
1346 foreach ($grades as $grade) {
1347 $grade->delete('userdelete');
1355 * Grading cron job. Performs background clean up on the gradebook
1357 function grade_cron() {
1363 FROM {grade_items} i
1364 WHERE i.locked = 0 AND i.locktime > 0 AND i.locktime < ? AND EXISTS (
1365 SELECT 'x' FROM {grade_items} c WHERE c.itemtype='course' AND c.needsupdate=0 AND c.courseid=i.courseid)";
1367 // go through all courses that have proper final grades and lock them if needed
1368 $rs = $DB->get_recordset_sql($sql, array($now));
1369 foreach ($rs as $item) {
1370 $grade_item = new grade_item($item, false);
1371 $grade_item->locked
= $now;
1372 $grade_item->update('locktime');
1376 $grade_inst = new grade_grade();
1377 $fields = 'g.'.implode(',g.', $grade_inst->required_fields
);
1379 $sql = "SELECT $fields
1380 FROM {grade_grades} g, {grade_items} i
1381 WHERE g.locked = 0 AND g.locktime > 0 AND g.locktime < ? AND g.itemid=i.id AND EXISTS (
1382 SELECT 'x' FROM {grade_items} c WHERE c.itemtype='course' AND c.needsupdate=0 AND c.courseid=i.courseid)";
1384 // go through all courses that have proper final grades and lock them if needed
1385 $rs = $DB->get_recordset_sql($sql, array($now));
1386 foreach ($rs as $grade) {
1387 $grade_grade = new grade_grade($grade, false);
1388 $grade_grade->locked
= $now;
1389 $grade_grade->update('locktime');
1393 //TODO: do not run this cleanup every cron invocation
1394 // cleanup history tables
1395 if (!empty($CFG->gradehistorylifetime
)) { // value in days
1396 $histlifetime = $now - ($CFG->gradehistorylifetime
* 3600 * 24);
1397 $tables = array('grade_outcomes_history', 'grade_categories_history', 'grade_items_history', 'grade_grades_history', 'scale_history');
1398 foreach ($tables as $table) {
1399 if ($DB->delete_records_select($table, "timemodified < ?", array($histlifetime))) {
1400 mtrace(" Deleted old grade history records from '$table'");
1407 * Reset all course grades, refetch from the activities and recalculate
1409 * @param int $courseid The course to reset
1410 * @return bool success
1412 function grade_course_reset($courseid) {
1414 // no recalculations
1415 grade_force_full_regrading($courseid);
1417 $grade_items = grade_item
::fetch_all(array('courseid'=>$courseid));
1418 foreach ($grade_items as $gid=>$grade_item) {
1419 $grade_item->delete_all_grades('reset');
1422 //refetch all grades
1423 grade_grab_course_grades($courseid);
1425 // recalculate all grades
1426 grade_regrade_final_grades($courseid);
1431 * Convert a number to 5 decimal point float, an empty string or a null db compatible format
1432 * (we need this to decide if db value changed)
1434 * @param mixed $number The number to convert
1435 * @return mixed float or null
1437 function grade_floatval($number) {
1438 if (is_null($number) or $number === '') {
1441 // we must round to 5 digits to get the same precision as in 10,5 db fields
1442 // note: db rounding for 10,5 is different from php round() function
1443 return round($number, 5);
1447 * Compare two float numbers safely. Uses 5 decimals php precision using {@link grade_floatval()}. Nulls accepted too.
1448 * Used for determining if a database update is required
1450 * @param float $f1 Float one to compare
1451 * @param float $f2 Float two to compare
1452 * @return bool True if the supplied values are different
1454 function grade_floats_different($f1, $f2) {
1455 // note: db rounding for 10,5 is different from php round() function
1456 return (grade_floatval($f1) !== grade_floatval($f2));
1460 * Compare two float numbers safely. Uses 5 decimals php precision using {@link grade_floatval()}
1462 * Do not use rounding for 10,5 at the database level as the results may be
1463 * different from php round() function.
1466 * @param float $f1 Float one to compare
1467 * @param float $f2 Float two to compare
1468 * @return bool True if the values should be considered as the same grades
1470 function grade_floats_equal($f1, $f2) {
1471 return (grade_floatval($f1) === grade_floatval($f2));