3 // This file is part of Moodle - http://moodle.org/
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.
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/>.
19 * Library of functions for gradebook - both public and internal
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 /////////////////////////////////////////////////////////////////////
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.
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)) {
84 } else if (count($grade_items) == 1){
85 $grade_item = reset($grade_items);
86 unset($grade_items); //release memory
89 debugging('Found more than one grade item');
90 return GRADE_UPDATE_MULTIPLE
;
93 if (!empty($itemdetails['deleted'])) {
95 if ($grade_item->delete($source)) {
96 return GRADE_UPDATE_OK
;
98 return GRADE_UPDATE_FAILED
;
101 return GRADE_UPDATE_OK
;
104 /// Create or update the grade_item if needed
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)) {
123 if ($k == 'gradetype' and $v == GRADE_TYPE_NONE
) {
124 // no grade item needed!
125 return GRADE_UPDATE_OK
;
130 $grade_item = new grade_item($params);
131 $grade_item->insert();
134 if ($grade_item->is_locked()) {
135 // no notice() here, test returned value instead!
136 return GRADE_UPDATE_ITEM_LOCKED
;
140 $itemdetails = (array)$itemdetails;
142 foreach ($itemdetails as $k=>$v) {
143 if (!in_array($k, $allowed)) {
147 if (in_array($k, $floats)) {
148 if (grade_floats_different($grade_item->{$k}, $v)) {
149 $grade_item->{$k} = $v;
154 if ($grade_item->{$k} != $v) {
155 $grade_item->{$k} = $v;
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);
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) {
199 if (empty($g['userid']) or $k != $g['userid']) {
200 debugging('Incorrect grade array index, must be user id! Grade ignored.');
205 if (empty($grades)) {
206 return GRADE_UPDATE_FAILED
;
209 $count = count($grades);
210 if ($count > 0 and $count < 200) {
211 list($uids, $params) = $DB->get_in_or_equal(array_keys($grades), SQL_PARAMS_NAMED
, $start='uid0');
212 $params['gid'] = $grade_item->id
;
213 $sql = "SELECT * FROM {grade_grades} WHERE itemid = :gid AND userid $uids";
216 $sql = "SELECT * FROM {grade_grades} WHERE itemid = :gid";
217 $params = array('gid'=>$grade_item->id
);
220 $rs = $DB->get_recordset_sql($sql, $params);
224 while (count($grades) > 0) {
228 foreach ($rs as $gd) {
230 $userid = $gd->userid
;
231 if (!isset($grades[$userid])) {
232 // this grade not requested, continue
235 // existing grade requested
236 $grade = $grades[$userid];
237 $grade_grade = new grade_grade($gd, false);
238 unset($grades[$userid]);
242 if (is_null($grade_grade)) {
243 if (count($grades) == 0) {
244 // no more grades to process
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]);
257 $feedbackformat = FORMAT_MOODLE
;
258 $usermodified = $USER->id
;
259 $datesubmitted = 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)) {
297 return GRADE_UPDATE_OK
;
299 return GRADE_UPDATE_FAILED
;
304 * Updates outcomes of user
305 * Manual outcomes can not be updated.
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))) {
320 foreach ($items as $item) {
321 if (!array_key_exists($item->itemnumber
, $data)) {
324 $grade = $data[$item->itemnumber
] < 1 ?
null : $data[$item->itemnumber
];
325 $result = ($item->update_final_grade($userid, $grade, $source) && $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.
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) {
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
:
382 case GRADE_TYPE_VALUE
:
386 case GRADE_TYPE_TEXT
:
390 $item->gradepass
= 0;
394 if (empty($userid_or_ids)) {
397 } else if (is_array($userid_or_ids)) {
398 $userids = $userid_or_ids;
401 $userids = array($userid_or_ids);
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
;
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
;
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
= '';
451 $grade->str_feedback
= format_text($grade->feedback
, $grade->feedbackformat
);
454 $item->grades
[$userid] = $grade;
457 $return->items
[$grade_item->itemnumber
] = $item;
460 if (!$grade_outcome = grade_outcome
::fetch(array('id'=>$grade_item->outcomeid
))) {
461 debugging('Incorect outcomeid found');
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)) {
475 } else if (is_array($userid_or_ids)) {
476 $userids = $userid_or_ids;
478 $userids = array($userid_or_ids);
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
)) {
501 $grade->str_grade
= get_string('nooutcome', 'grades');
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
= '';
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))) {
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
);
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
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) {
567 static $cache = array();
569 if ($resetcache or !array_key_exists($courseid, $cache)) {
570 $cache[$courseid] = array();
572 } else if (is_null($name)) {
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))) {
582 $result = $data->value
;
585 if (is_null($result)) {
589 $cache[$courseid][$name] = $result;
594 * Returns all course gradebook settings as object properties
597 * @param int $courseid
600 function grade_get_settings($courseid) {
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
;
616 * Add/update course gradebook setting
619 * @param int $courseid
620 * @param string $name of setting
621 * @param string value, NULL means no setting==remove
624 function grade_set_setting($courseid, $name, $value) {
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;
634 $data->value
= $value;
635 $DB->insert_record('grade_settings', $data);
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
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
) {
663 if (is_null($value)) {
667 if ($grade_item->gradetype
!= GRADE_TYPE_VALUE
and $grade_item->gradetype
!= GRADE_TYPE_SCALE
) {
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) . ')';
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]);
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
;
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)) {
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) {
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!');
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]);
795 $result[-1] = get_string('newcategory', 'grades');
798 foreach ($categories as $category) {
799 $cats[$category->id
] = $category->get_name();
801 textlib_get_instance()->asort($cats);
803 return ($result+
$cats);
807 * Returns grade letters array used in context
809 * @param object $context object or null for defaults
810 * @return array of grade_boundary=>letter_string
812 function grade_get_letters($context=null) {
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
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;
848 $letters = grade_get_letters(null);
849 $cache[$context->id
] = $letters;
855 * Verify new value of idnumber - checks for uniqueness of new idnumbers, old are kept intact
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) {
867 if ($idnumber == '') {
868 //we allow empty idnumbers
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
880 } else if ($grade_item and $grade_item->idnumber
== $idnumber) {
884 if ($DB->record_exists('course_modules', array('course'=>$courseid, 'idnumber'=>$idnumber))) {
888 if ($DB->record_exists('grade_items', array('courseid'=>$courseid, 'idnumber'=>$idnumber))) {
896 * Force final grade recalculation in all course items
899 * @param int $courseid
901 function grade_force_full_regrading($courseid) {
903 $DB->set_field('grade_items', 'needsupdate', 1, array('courseid'=>$courseid));
907 * Forces regrading of all site grades - usualy when chanign site setings
911 function grade_force_site_regrading() {
913 $DB->set_field('grade_items', 'needsupdate', 1);
917 * Updates all final grades in course.
919 * @param int $courseid
920 * @param int $userid if specified, try to do a quick regrading of grades of this user only
921 * @param object $updated_item the item in which
922 * @return boolean true if ok, array of errors if problems found (item id is used as key)
924 function grade_regrade_final_grades($courseid, $userid=null, $updated_item=null) {
926 $course_item = grade_item
::fetch_course_item($courseid);
929 // one raw grade updated for one user
930 if (empty($updated_item)) {
931 print_error("cannotbenull", 'debug', '', "updated_item");
933 if ($course_item->needsupdate
) {
934 $updated_item->force_regrading();
935 return array($course_item->id
=>'Can not do fast regrading after updating of raw grades');
939 if (!$course_item->needsupdate
) {
945 $grade_items = grade_item
::fetch_all(array('courseid'=>$courseid));
946 $depends_on = array();
948 // first mark all category and calculated items as needing regrading
949 // this is slower, but 100% accurate
950 foreach ($grade_items as $gid=>$gitem) {
951 if (!empty($updated_item) and $updated_item->id
== $gid) {
952 $grade_items[$gid]->needsupdate
= 1;
954 } else if ($gitem->is_course_item() or $gitem->is_category_item() or $gitem->is_calculated()) {
955 $grade_items[$gid]->needsupdate
= 1;
958 // construct depends_on lookup array
959 $depends_on[$gid] = $grade_items[$gid]->depends_on();
964 $gids = array_keys($grade_items);
967 while (count($finalids) < count($gids)) { // work until all grades are final or error found
969 foreach ($gids as $gid) {
970 if (in_array($gid, $finalids)) {
971 continue; // already final
974 if (!$grade_items[$gid]->needsupdate
) {
975 $finalids[] = $gid; // we can make it final - does not need update
980 foreach ($depends_on[$gid] as $did) {
981 if (!in_array($did, $finalids)) {
983 continue; // this item depends on something that is not yet in finals array
987 //oki - let's update, calculate or aggregate :-)
989 $result = $grade_items[$gid]->regrade_final_grades($userid);
991 if ($result === true) {
992 $grade_items[$gid]->regrading_finished();
993 $grade_items[$gid]->check_locktime(); // do the locktime item locking
998 $grade_items[$gid]->force_regrading();
999 $errors[$gid] = $result;
1011 foreach($gids as $gid) {
1012 if (in_array($gid, $finalids)) {
1013 continue; // this one is ok
1015 $grade_items[$gid]->force_regrading();
1016 $errors[$grade_items[$gid]->id
] = 'Probably circular reference or broken calculation formula'; // TODO: localize
1018 break; // oki, found error
1022 if (count($errors) == 0) {
1023 if (empty($userid)) {
1024 // do the locktime locking of grades, but only when doing full regrading
1025 grade_grade
::check_locktime_all($gids);
1034 * Refetches data from all course activities
1038 * @param int $courseid
1039 * @param string $modname
1042 function grade_grab_course_grades($courseid, $modname=null) {
1046 $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname
1047 FROM {".$modname."} a, {course_modules} cm, {modules} m
1048 WHERE m.name=:modname AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=:courseid";
1049 $params = array('modname'=>$modname, 'courseid'=>$courseid);
1051 if ($modinstances = $DB->get_records_sql($sql, $params)) {
1052 foreach ($modinstances as $modinstance) {
1053 grade_update_mod_grades($modinstance);
1059 if (!$mods = get_plugin_list('mod') ) {
1060 print_error('nomodules', 'debug');
1063 foreach ($mods as $mod => $fullmod) {
1064 if ($mod == 'NEWMODULE') { // Someone has unzipped the template, ignore it
1068 // include the module lib once
1069 if (file_exists($fullmod.'/lib.php')) {
1070 // get all instance of the activity
1071 $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname
1072 FROM {".$mod."} a, {course_modules} cm, {modules} m
1073 WHERE m.name=:mod AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=:courseid";
1074 $params = array('mod'=>$mod, 'courseid'=>$courseid);
1076 if ($modinstances = $DB->get_records_sql($sql, $params)) {
1077 foreach ($modinstances as $modinstance) {
1078 grade_update_mod_grades($modinstance);
1086 * Force full update of module grades in central gradebook
1090 * @param object $modinstance object with extra cmidnumber and modname property
1091 * @param int $userid
1092 * @return boolean success
1094 function grade_update_mod_grades($modinstance, $userid=0) {
1097 $fullmod = $CFG->dirroot
.'/mod/'.$modinstance->modname
;
1098 if (!file_exists($fullmod.'/lib.php')) {
1099 debugging('missing lib.php file in module ' . $modinstance->modname
);
1102 include_once($fullmod.'/lib.php');
1104 $updategradesfunc = $modinstance->modname
.'_update_grades';
1105 $updateitemfunc = $modinstance->modname
.'_grade_item_update';
1107 if (function_exists($updategradesfunc) and function_exists($updateitemfunc)) {
1108 //new grading supported, force updating of grades
1109 $updateitemfunc($modinstance);
1110 $updategradesfunc($modinstance, $userid);
1113 // mudule does not support grading??
1120 * Remove grade letters for given context
1123 * @param object $context
1124 * @param bool $showfeedback
1126 function remove_grade_letters($context, $showfeedback) {
1127 global $DB, $OUTPUT;
1129 $strdeleted = get_string('deleted');
1131 $DB->delete_records('grade_letters', array('contextid'=>$context->id
));
1132 if ($showfeedback) {
1133 echo $OUTPUT->notification($strdeleted.' - '.get_string('letters', 'grades'));
1137 * Remove all grade related course data - history is kept
1140 * @param int $courseid
1141 * @param bool $showfeedback print feedback
1143 function remove_course_grades($courseid, $showfeedback) {
1144 global $DB, $OUTPUT;
1146 $strdeleted = get_string('deleted');
1148 $course_category = grade_category
::fetch_course_category($courseid);
1149 $course_category->delete('coursedelete');
1150 if ($showfeedback) {
1151 echo $OUTPUT->notification($strdeleted.' - '.get_string('grades', 'grades').', '.get_string('items', 'grades').', '.get_string('categories', 'grades'));
1154 if ($outcomes = grade_outcome
::fetch_all(array('courseid'=>$courseid))) {
1155 foreach ($outcomes as $outcome) {
1156 $outcome->delete('coursedelete');
1159 $DB->delete_records('grade_outcomes_courses', array('courseid'=>$courseid));
1160 if ($showfeedback) {
1161 echo $OUTPUT->notification($strdeleted.' - '.get_string('outcomes', 'grades'));
1164 if ($scales = grade_scale
::fetch_all(array('courseid'=>$courseid))) {
1165 foreach ($scales as $scale) {
1166 $scale->delete('coursedelete');
1169 if ($showfeedback) {
1170 echo $OUTPUT->notification($strdeleted.' - '.get_string('scales'));
1173 $DB->delete_records('grade_settings', array('courseid'=>$courseid));
1174 if ($showfeedback) {
1175 echo $OUTPUT->notification($strdeleted.' - '.get_string('settings', 'grades'));
1180 * Called when course category deleted - cleanup gradebook
1183 * @param int $categoryid course category id
1184 * @param int $newparentid empty means everything deleted, otherwise id of category where content moved
1185 * @param bool $showfeedback print feedback
1187 function grade_course_category_delete($categoryid, $newparentid, $showfeedback) {
1190 $context = get_context_instance(CONTEXT_COURSECAT
, $categoryid);
1191 $DB->delete_records('grade_letters', array('contextid'=>$context->id
));
1195 * Does gradebook cleanup when module uninstalled.
1199 * @param string $modname
1201 function grade_uninstalled_module($modname) {
1206 WHERE itemtype='mod' AND itemmodule=?";
1208 // go all items for this module and delete them including the grades
1209 $rs = $DB->get_recordset_sql($sql, array($modname));
1210 foreach ($rs as $item) {
1211 $grade_item = new grade_item($item, false);
1212 $grade_item->delete('moduninstall');
1218 * Deletes all user data from gradebook.
1221 function grade_user_delete($userid) {
1222 if ($grades = grade_grade
::fetch_all(array('userid'=>$userid))) {
1223 foreach ($grades as $grade) {
1224 $grade->delete('userdelete');
1230 * Purge course data when user unenrolled.
1233 function grade_user_unenrol($courseid, $userid) {
1234 if ($items = grade_item
::fetch_all(array('courseid'=>$courseid))) {
1235 foreach ($items as $item) {
1236 if ($grades = grade_grade
::fetch_all(array('userid'=>$userid, 'itemid'=>$item->id
))) {
1237 foreach ($grades as $grade) {
1238 $grade->delete('userdelete');
1251 function grade_cron() {
1257 FROM {grade_items} i
1258 WHERE i.locked = 0 AND i.locktime > 0 AND i.locktime < ? AND EXISTS (
1259 SELECT 'x' FROM {grade_items} c WHERE c.itemtype='course' AND c.needsupdate=0 AND c.courseid=i.courseid)";
1261 // go through all courses that have proper final grades and lock them if needed
1262 $rs = $DB->get_recordset_sql($sql, array($now));
1263 foreach ($rs as $item) {
1264 $grade_item = new grade_item($item, false);
1265 $grade_item->locked
= $now;
1266 $grade_item->update('locktime');
1270 $grade_inst = new grade_grade();
1271 $fields = 'g.'.implode(',g.', $grade_inst->required_fields
);
1273 $sql = "SELECT $fields
1274 FROM {grade_grades} g, {grade_items} i
1275 WHERE g.locked = 0 AND g.locktime > 0 AND g.locktime < ? AND g.itemid=i.id AND EXISTS (
1276 SELECT 'x' FROM {grade_items} c WHERE c.itemtype='course' AND c.needsupdate=0 AND c.courseid=i.courseid)";
1278 // go through all courses that have proper final grades and lock them if needed
1279 $rs = $DB->get_recordset_sql($sql, array($now));
1280 foreach ($rs as $grade) {
1281 $grade_grade = new grade_grade($grade, false);
1282 $grade_grade->locked
= $now;
1283 $grade_grade->update('locktime');
1287 //TODO: do not run this cleanup every cron invocation
1288 // cleanup history tables
1289 if (!empty($CFG->gradehistorylifetime
)) { // value in days
1290 $histlifetime = $now - ($CFG->gradehistorylifetime
* 3600 * 24);
1291 $tables = array('grade_outcomes_history', 'grade_categories_history', 'grade_items_history', 'grade_grades_history', 'scale_history');
1292 foreach ($tables as $table) {
1293 if ($DB->delete_records_select($table, "timemodified < ?", array($histlifetime))) {
1294 mtrace(" Deleted old grade history records from '$table'");
1301 * Resel all course grades
1303 * @param int $courseid
1304 * @return bool success
1306 function grade_course_reset($courseid) {
1308 // no recalculations
1309 grade_force_full_regrading($courseid);
1311 $grade_items = grade_item
::fetch_all(array('courseid'=>$courseid));
1312 foreach ($grade_items as $gid=>$grade_item) {
1313 $grade_item->delete_all_grades('reset');
1316 //refetch all grades
1317 grade_grab_course_grades($courseid);
1319 // recalculate all grades
1320 grade_regrade_final_grades($courseid);
1325 * Convert number to 5 decimalfloat, empty string or null db compatible format
1326 * (we need this to decide if db value changed)
1328 * @param mixed $number
1329 * @return mixed float or null
1331 function grade_floatval($number) {
1332 if (is_null($number) or $number === '') {
1335 // we must round to 5 digits to get the same precision as in 10,5 db fields
1336 // note: db rounding for 10,5 is different from php round() function
1337 return round($number, 5);
1341 * Compare two float numbers safely. Uses 5 decimals php precision. Nulls accepted too.
1342 * Used for skipping of db updates
1346 * @return bool true if different
1348 function grade_floats_different($f1, $f2) {
1349 // note: db rounding for 10,5 is different from php round() function
1350 return (grade_floatval($f1) !== grade_floatval($f2));
1354 * Compare two float numbers safely. Uses 5 decimals php precision.
1356 * Do not use rounding for 10,5 at the database level as the results may be
1357 * different from php round() function.
1362 * @return bool true if the values should be considered as the same grades
1364 function grade_floats_equal($f1, $f2) {
1365 return (grade_floatval($f1) === grade_floatval($f2));