MDL-30085 core_grades functions moved to correct location
[moodle.git] / lib / gradelib.php
CommitLineData
ba21c9d4 1<?php
117bd748
PS
2// This file is part of Moodle - http://moodle.org/
3//
ba21c9d4 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.
117bd748 13//
ba21c9d4 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/>.
5834dcdb 16
17/**
b9f49659 18 * Library of functions for gradebook - both public and internal
5834dcdb 19 *
a153c9f2
AD
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
5834dcdb 23 */
24
78bfb562
PS
25defined('MOODLE_INTERNAL') || die();
26
27/** Include essential files */
53461661 28require_once($CFG->libdir . '/grade/constants.php');
eea6690a 29
3058964f 30require_once($CFG->libdir . '/grade/grade_category.php');
31require_once($CFG->libdir . '/grade/grade_item.php');
3ee5c201 32require_once($CFG->libdir . '/grade/grade_grade.php');
d5bdb228 33require_once($CFG->libdir . '/grade/grade_scale.php');
5501446d 34require_once($CFG->libdir . '/grade/grade_outcome.php');
60cf7430 35
b9f49659 36/////////////////////////////////////////////////////////////////////
37///// Start of public API for communication with modules/blocks /////
38/////////////////////////////////////////////////////////////////////
612607bd 39
c5b5f18d 40/**
41 * Submit new or update grade; update/create grade_item definition. Grade must have userid specified,
a153c9f2
AD
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.
4cf1b9be 44 *
c5b5f18d 45 * Only following grade item properties can be changed 'itemname', 'idnumber', 'gradetype', 'grademax',
f0362b5d 46 * 'grademin', 'scaleid', 'multfactor', 'plusfactor', 'deleted' and 'hidden'. 'reset' means delete all current grades including locked ones.
4cf1b9be 47 *
fcac8e51 48 * Manual, course or category items can not be updated by this function.
a153c9f2
AD
49 *
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
ca540697 59 * @return int Returns GRADE_UPDATE_OK, GRADE_UPDATE_FAILED, GRADE_UPDATE_MULTIPLE or GRADE_UPDATE_ITEM_LOCKED
c5b5f18d 60 */
b67ec72f 61function grade_update($source, $courseid, $itemtype, $itemmodule, $iteminstance, $itemnumber, $grades=NULL, $itemdetails=NULL) {
9718765e 62 global $USER, $CFG, $DB;
612607bd 63
c5b5f18d 64 // only following grade_item properties can be changed in this function
1223d24a 65 $allowed = array('itemname', 'idnumber', 'gradetype', 'grademax', 'grademin', 'scaleid', 'multfactor', 'plusfactor', 'deleted', 'hidden');
25bcd908 66 // list of 10,5 numeric fields
67 $floats = array('grademin', 'grademax', 'multfactor', 'plusfactor');
612607bd 68
c4e4068f 69 // grade item identification
70 $params = compact('courseid', 'itemtype', 'itemmodule', 'iteminstance', 'itemnumber');
71
612607bd 72 if (is_null($courseid) or is_null($itemtype)) {
73 debugging('Missing courseid or itemtype');
74 return GRADE_UPDATE_FAILED;
75 }
76
c4e4068f 77 if (!$grade_items = grade_item::fetch_all($params)) {
612607bd 78 // create a new one
79 $grade_item = false;
80
81 } else if (count($grade_items) == 1){
82 $grade_item = reset($grade_items);
83 unset($grade_items); //release memory
84
85 } else {
34e67f76 86 debugging('Found more than one grade item');
612607bd 87 return GRADE_UPDATE_MULTIPLE;
88 }
89
aaff71da 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;
96 }
97 }
98 return GRADE_UPDATE_OK;
99 }
100
612607bd 101/// Create or update the grade_item if needed
b159da78 102
612607bd 103 if (!$grade_item) {
612607bd 104 if ($itemdetails) {
105 $itemdetails = (array)$itemdetails;
2e53372c 106
772ddfbf 107 // grademin and grademax ignored when scale specified
2e53372c 108 if (array_key_exists('scaleid', $itemdetails)) {
109 if ($itemdetails['scaleid']) {
110 unset($itemdetails['grademin']);
111 unset($itemdetails['grademax']);
112 }
113 }
114
612607bd 115 foreach ($itemdetails as $k=>$v) {
116 if (!in_array($k, $allowed)) {
117 // ignore it
118 continue;
119 }
120 if ($k == 'gradetype' and $v == GRADE_TYPE_NONE) {
121 // no grade item needed!
122 return GRADE_UPDATE_OK;
123 }
124 $params[$k] = $v;
125 }
126 }
f70152b7 127 $grade_item = new grade_item($params);
128 $grade_item->insert();
612607bd 129
130 } else {
2cc4b0f9 131 if ($grade_item->is_locked()) {
d6bc2a81 132 // no notice() here, test returned value instead!
678e8898 133 return GRADE_UPDATE_ITEM_LOCKED;
612607bd 134 }
135
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;
143 }
25bcd908 144 if (in_array($k, $floats)) {
145 if (grade_floats_different($grade_item->{$k}, $v)) {
146 $grade_item->{$k} = $v;
147 $update = true;
148 }
55231be0 149
25bcd908 150 } else {
151 if ($grade_item->{$k} != $v) {
152 $grade_item->{$k} = $v;
153 $update = true;
154 }
612607bd 155 }
156 }
157 if ($update) {
158 $grade_item->update();
159 }
160 }
161 }
162
f0362b5d 163/// reset grades if requested
164 if (!empty($itemdetails['reset'])) {
165 $grade_item->delete_all_grades('reset');
166 return GRADE_UPDATE_OK;
167 }
168
612607bd 169/// Some extra checks
170 // do we use grading?
171 if ($grade_item->gradetype == GRADE_TYPE_NONE) {
172 return GRADE_UPDATE_OK;
173 }
174
175 // no grade submitted
b67ec72f 176 if (empty($grades)) {
612607bd 177 return GRADE_UPDATE_OK;
178 }
179
612607bd 180/// Finally start processing of grades
b67ec72f 181 if (is_object($grades)) {
55231be0 182 $grades = array($grades->userid=>$grades);
612607bd 183 } else {
b67ec72f 184 if (array_key_exists('userid', $grades)) {
55231be0 185 $grades = array($grades['userid']=>$grades);
186 }
187 }
188
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;
612607bd 194 }
55231be0 195
af454fa5 196 if (empty($g['userid']) or $k != $g['userid']) {
55231be0 197 debugging('Incorrect grade array index, must be user id! Grade ignored.');
198 unset($grades[$k]);
199 }
200 }
201
202 if (empty($grades)) {
203 return GRADE_UPDATE_FAILED;
204 }
205
206 $count = count($grades);
9718765e 207 if ($count > 0 and $count < 200) {
cf717dc2 208 list($uids, $params) = $DB->get_in_or_equal(array_keys($grades), SQL_PARAMS_NAMED, $start='uid');
9718765e 209 $params['gid'] = $grade_item->id;
210 $sql = "SELECT * FROM {grade_grades} WHERE itemid = :gid AND userid $uids";
55231be0 211
212 } else {
9718765e 213 $sql = "SELECT * FROM {grade_grades} WHERE itemid = :gid";
214 $params = array('gid'=>$grade_item->id);
612607bd 215 }
216
9718765e 217 $rs = $DB->get_recordset_sql($sql, $params);
55231be0 218
4cf1b9be 219 $failed = false;
55231be0 220
221 while (count($grades) > 0) {
222 $grade_grade = null;
223 $grade = null;
224
9cf4a18b 225 foreach ($rs as $gd) {
226
55231be0 227 $userid = $gd->userid;
228 if (!isset($grades[$userid])) {
229 // this grade not requested, continue
230 continue;
231 }
232 // existing grade requested
233 $grade = $grades[$userid];
234 $grade_grade = new grade_grade($gd, false);
235 unset($grades[$userid]);
236 break;
237 }
238
239 if (is_null($grade_grade)) {
240 if (count($grades) == 0) {
241 // no more grades to process
242 break;
243 }
244
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]);
612607bd 250 }
251
2cc4b0f9 252 $rawgrade = false;
ac9b0805 253 $feedback = false;
254 $feedbackformat = FORMAT_MOODLE;
ced5ee59 255 $usermodified = $USER->id;
256 $datesubmitted = null;
257 $dategraded = null;
772ddfbf 258
ac9b0805 259 if (array_key_exists('rawgrade', $grade)) {
260 $rawgrade = $grade['rawgrade'];
261 }
612607bd 262
4cf1b9be 263 if (array_key_exists('feedback', $grade)) {
ac9b0805 264 $feedback = $grade['feedback'];
612607bd 265 }
266
ac9b0805 267 if (array_key_exists('feedbackformat', $grade)) {
268 $feedbackformat = $grade['feedbackformat'];
612607bd 269 }
270
aaff71da 271 if (array_key_exists('usermodified', $grade)) {
272 $usermodified = $grade['usermodified'];
ced5ee59 273 }
274
275 if (array_key_exists('datesubmitted', $grade)) {
276 $datesubmitted = $grade['datesubmitted'];
277 }
278
279 if (array_key_exists('dategraded', $grade)) {
280 $dategraded = $grade['dategraded'];
aaff71da 281 }
282
ac9b0805 283 // update or insert the grade
55231be0 284 if (!$grade_item->update_raw_grade($userid, $rawgrade, $source, $feedback, $feedbackformat, $usermodified, $dategraded, $datesubmitted, $grade_grade)) {
4cf1b9be 285 $failed = true;
4cf1b9be 286 }
612607bd 287 }
288
55231be0 289 if ($rs) {
9718765e 290 $rs->close();
55231be0 291 }
292
4cf1b9be 293 if (!$failed) {
294 return GRADE_UPDATE_OK;
295 } else {
296 return GRADE_UPDATE_FAILED;
297 }
612607bd 298}
299
3a5ae660 300/**
a153c9f2 301 * Updates a user's outcomes. Manual outcomes can not be updated.
ba21c9d4 302 *
a153c9f2
AD
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
a4d76049 310 * @param array $data Array consisting of grade item itemnumber ({@link grade_update()}) => outcomegrade
a153c9f2 311 * @return bool returns true if grade items were found and updated successfully
3a5ae660 312 */
313function 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))) {
a51897d8 315 $result = true;
3a5ae660 316 foreach ($items as $item) {
d886a7ea 317 if (!array_key_exists($item->itemnumber, $data)) {
3a5ae660 318 continue;
319 }
d886a7ea 320 $grade = $data[$item->itemnumber] < 1 ? null : $data[$item->itemnumber];
a51897d8 321 $result = ($item->update_final_grade($userid, $grade, $source) && $result);
11a14999 322 }
a51897d8 323 return $result;
3a5ae660 324 }
f5a29953 325 return false; //grade items not found
3a5ae660 326}
327
6b5c722d 328/**
d3549931 329 * Returns grading information for one or more activities, optionally with user grades
fcac8e51 330 * Manual, course or category items can not be queried.
ba21c9d4 331 *
a153c9f2
AD
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
ba21c9d4 338 * @return array Array of grade information objects (scaleid, name, grade and locked status, etc.) indexed with itemnumbers
6b5c722d 339 */
d3549931 340function grade_get_grades($courseid, $itemtype = null, $itemmodule = null, $iteminstance = null, $userid_or_ids=null) {
a3fbd494 341 global $CFG;
342
365a5941 343 $return = new stdClass();
fcac8e51 344 $return->items = array();
345 $return->outcomes = array();
6b5c722d 346
fcac8e51 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);
353 }
354 }
6b5c722d 355
198e802f 356 $params = array('courseid' => $courseid);
d3549931
AD
357 if (!empty($itemtype)) {
358 $params['itemtype'] = $itemtype;
359 }
360 if (!empty($itemmodule)) {
361 $params['itemmodule'] = $itemmodule;
362 }
363 if (!empty($iteminstance)) {
364 $params['iteminstance'] = $iteminstance;
365 }
366 if ($grade_items = grade_item::fetch_all($params)) {
fcac8e51 367 foreach ($grade_items as $grade_item) {
a3fbd494 368 $decimalpoints = null;
369
fcac8e51 370 if (empty($grade_item->outcomeid)) {
371 // prepare information about grade item
365a5941 372 $item = new stdClass();
d3549931 373 $item->id = $grade_item->id;
fcac8e51 374 $item->itemnumber = $grade_item->itemnumber;
d3549931
AD
375 $item->itemtype = $grade_item->itemtype;
376 $item->itemmodule = $grade_item->itemmodule;
377 $item->iteminstance = $grade_item->iteminstance;
fcac8e51 378 $item->scaleid = $grade_item->scaleid;
379 $item->name = $grade_item->get_name();
380 $item->grademin = $grade_item->grademin;
381 $item->grademax = $grade_item->grademax;
382 $item->gradepass = $grade_item->gradepass;
383 $item->locked = $grade_item->is_locked();
384 $item->hidden = $grade_item->is_hidden();
385 $item->grades = array();
386
387 switch ($grade_item->gradetype) {
388 case GRADE_TYPE_NONE:
389 continue;
6b5c722d 390
6b5c722d 391 case GRADE_TYPE_VALUE:
fcac8e51 392 $item->scaleid = 0;
6b5c722d 393 break;
394
fcac8e51 395 case GRADE_TYPE_TEXT:
396 $item->scaleid = 0;
397 $item->grademin = 0;
398 $item->grademax = 0;
399 $item->gradepass = 0;
6b5c722d 400 break;
fcac8e51 401 }
6b5c722d 402
fcac8e51 403 if (empty($userid_or_ids)) {
404 $userids = array();
405
406 } else if (is_array($userid_or_ids)) {
407 $userids = $userid_or_ids;
408
409 } else {
410 $userids = array($userid_or_ids);
6b5c722d 411 }
6b5c722d 412
fcac8e51 413 if ($userids) {
414 $grade_grades = grade_grade::fetch_users_grades($grade_item, $userids, true);
415 foreach ($userids as $userid) {
416 $grade_grades[$userid]->grade_item =& $grade_item;
417
365a5941 418 $grade = new stdClass();
fcac8e51 419 $grade->grade = $grade_grades[$userid]->finalgrade;
420 $grade->locked = $grade_grades[$userid]->is_locked();
421 $grade->hidden = $grade_grades[$userid]->is_hidden();
422 $grade->overridden = $grade_grades[$userid]->overridden;
423 $grade->feedback = $grade_grades[$userid]->feedback;
424 $grade->feedbackformat = $grade_grades[$userid]->feedbackformat;
a3fbd494 425 $grade->usermodified = $grade_grades[$userid]->usermodified;
ced5ee59 426 $grade->datesubmitted = $grade_grades[$userid]->get_datesubmitted();
427 $grade->dategraded = $grade_grades[$userid]->get_dategraded();
fcac8e51 428
429 // create text representation of grade
5048575d 430 if ($grade_item->gradetype == GRADE_TYPE_TEXT or $grade_item->gradetype == GRADE_TYPE_NONE) {
431 $grade->grade = null;
432 $grade->str_grade = '-';
433 $grade->str_long_grade = $grade->str_grade;
55231be0 434
5048575d 435 } else if (in_array($grade_item->id, $needsupdate)) {
85a0a69f 436 $grade->grade = false;
437 $grade->str_grade = get_string('error');
438 $grade->str_long_grade = $grade->str_grade;
fcac8e51 439
440 } else if (is_null($grade->grade)) {
85a0a69f 441 $grade->str_grade = '-';
442 $grade->str_long_grade = $grade->str_grade;
fcac8e51 443
444 } else {
e9096dc2 445 $grade->str_grade = grade_format_gradevalue($grade->grade, $grade_item);
85a0a69f 446 if ($grade_item->gradetype == GRADE_TYPE_SCALE or $grade_item->get_displaytype() != GRADE_DISPLAY_TYPE_REAL) {
447 $grade->str_long_grade = $grade->str_grade;
448 } else {
365a5941 449 $a = new stdClass();
85a0a69f 450 $a->grade = $grade->str_grade;
451 $a->max = grade_format_gradevalue($grade_item->grademax, $grade_item);
452 $grade->str_long_grade = get_string('gradelong', 'grades', $a);
453 }
fcac8e51 454 }
455
456 // create html representation of feedback
457 if (is_null($grade->feedback)) {
458 $grade->str_feedback = '';
459 } else {
460 $grade->str_feedback = format_text($grade->feedback, $grade->feedbackformat);
461 }
462
463 $item->grades[$userid] = $grade;
464 }
465 }
466 $return->items[$grade_item->itemnumber] = $item;
467
6b5c722d 468 } else {
fcac8e51 469 if (!$grade_outcome = grade_outcome::fetch(array('id'=>$grade_item->outcomeid))) {
470 debugging('Incorect outcomeid found');
471 continue;
472 }
473
474 // outcome info
365a5941 475 $outcome = new stdClass();
d3549931 476 $outcome->id = $grade_item->id;
fcac8e51 477 $outcome->itemnumber = $grade_item->itemnumber;
d3549931
AD
478 $outcome->itemtype = $grade_item->itemtype;
479 $outcome->itemmodule = $grade_item->itemmodule;
480 $outcome->iteminstance = $grade_item->iteminstance;
fcac8e51 481 $outcome->scaleid = $grade_outcome->scaleid;
482 $outcome->name = $grade_outcome->get_name();
483 $outcome->locked = $grade_item->is_locked();
484 $outcome->hidden = $grade_item->is_hidden();
485
486 if (empty($userid_or_ids)) {
487 $userids = array();
488 } else if (is_array($userid_or_ids)) {
489 $userids = $userid_or_ids;
490 } else {
491 $userids = array($userid_or_ids);
492 }
6b5c722d 493
fcac8e51 494 if ($userids) {
495 $grade_grades = grade_grade::fetch_users_grades($grade_item, $userids, true);
496 foreach ($userids as $userid) {
497 $grade_grades[$userid]->grade_item =& $grade_item;
498
365a5941 499 $grade = new stdClass();
fcac8e51 500 $grade->grade = $grade_grades[$userid]->finalgrade;
501 $grade->locked = $grade_grades[$userid]->is_locked();
502 $grade->hidden = $grade_grades[$userid]->is_hidden();
503 $grade->feedback = $grade_grades[$userid]->feedback;
504 $grade->feedbackformat = $grade_grades[$userid]->feedbackformat;
a3fbd494 505 $grade->usermodified = $grade_grades[$userid]->usermodified;
fcac8e51 506
507 // create text representation of grade
508 if (in_array($grade_item->id, $needsupdate)) {
509 $grade->grade = false;
510 $grade->str_grade = get_string('error');
511
512 } else if (is_null($grade->grade)) {
513 $grade->grade = 0;
514 $grade->str_grade = get_string('nooutcome', 'grades');
515
516 } else {
517 $grade->grade = (int)$grade->grade;
518 $scale = $grade_item->load_scale();
519 $grade->str_grade = format_string($scale->scale_items[(int)$grade->grade-1]);
520 }
521
522 // create html representation of feedback
523 if (is_null($grade->feedback)) {
524 $grade->str_feedback = '';
525 } else {
526 $grade->str_feedback = format_text($grade->feedback, $grade->feedbackformat);
527 }
528
529 $outcome->grades[$userid] = $grade;
530 }
531 }
d19f4770 532
533 if (isset($return->outcomes[$grade_item->itemnumber])) {
534 // itemnumber duplicates - lets fix them!
535 $newnumber = $grade_item->itemnumber + 1;
536 while(grade_item::fetch(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid, 'itemnumber'=>$newnumber))) {
537 $newnumber++;
538 }
539 $outcome->itemnumber = $newnumber;
540 $grade_item->itemnumber = $newnumber;
541 $grade_item->update('system');
542 }
543
d886a7ea 544 $return->outcomes[$grade_item->itemnumber] = $outcome;
fcac8e51 545
546 }
6b5c722d 547 }
548 }
549
fcac8e51 550 // sort results using itemnumbers
551 ksort($return->items, SORT_NUMERIC);
552 ksort($return->outcomes, SORT_NUMERIC);
553
554 return $return;
6b5c722d 555}
556
b9f49659 557///////////////////////////////////////////////////////////////////
558///// End of public API for communication with modules/blocks /////
559///////////////////////////////////////////////////////////////////
77dbe708 560
77dbe708 561
612607bd 562
b9f49659 563///////////////////////////////////////////////////////////////////
564///// Internal API: used by gradebook plugins and Moodle core /////
565///////////////////////////////////////////////////////////////////
e0724506 566
567/**
a153c9f2 568 * Returns a course gradebook setting
ba21c9d4 569 *
e0724506 570 * @param int $courseid
571 * @param string $name of setting, maybe null if reset only
a153c9f2 572 * @param string $default value to return if setting is not found
e0724506 573 * @param bool $resetcache force reset of internal static cache
a153c9f2 574 * @return string value of the setting, $default if setting not found, NULL if supplied $name is null
e0724506 575 */
576function grade_get_setting($courseid, $name, $default=null, $resetcache=false) {
9718765e 577 global $DB;
578
e0724506 579 static $cache = array();
580
581 if ($resetcache or !array_key_exists($courseid, $cache)) {
582 $cache[$courseid] = array();
583
584 } else if (is_null($name)) {
585 return null;
586
587 } else if (array_key_exists($name, $cache[$courseid])) {
588 return $cache[$courseid][$name];
589 }
590
9718765e 591 if (!$data = $DB->get_record('grade_settings', array('courseid'=>$courseid, 'name'=>$name))) {
e0724506 592 $result = null;
593 } else {
594 $result = $data->value;
595 }
596
597 if (is_null($result)) {
598 $result = $default;
599 }
600
601 $cache[$courseid][$name] = $result;
602 return $result;
603}
604
26ed0305 605/**
606 * Returns all course gradebook settings as object properties
ba21c9d4 607 *
26ed0305 608 * @param int $courseid
609 * @return object
610 */
611function grade_get_settings($courseid) {
9718765e 612 global $DB;
613
365a5941 614 $settings = new stdClass();
26ed0305 615 $settings->id = $courseid;
616
9718765e 617 if ($records = $DB->get_records('grade_settings', array('courseid'=>$courseid))) {
26ed0305 618 foreach ($records as $record) {
619 $settings->{$record->name} = $record->value;
620 }
621 }
622
623 return $settings;
624}
625
e0724506 626/**
a153c9f2 627 * Add, update or delete a course gradebook setting
ba21c9d4 628 *
a153c9f2
AD
629 * @param int $courseid The course ID
630 * @param string $name Name of the setting
631 * @param string $value Value of the setting. NULL means delete the setting.
e0724506 632 */
633function grade_set_setting($courseid, $name, $value) {
9718765e 634 global $DB;
635
e0724506 636 if (is_null($value)) {
9718765e 637 $DB->delete_records('grade_settings', array('courseid'=>$courseid, 'name'=>$name));
e0724506 638
9718765e 639 } else if (!$existing = $DB->get_record('grade_settings', array('courseid'=>$courseid, 'name'=>$name))) {
365a5941 640 $data = new stdClass();
e0724506 641 $data->courseid = $courseid;
9718765e 642 $data->name = $name;
643 $data->value = $value;
644 $DB->insert_record('grade_settings', $data);
e0724506 645
646 } else {
365a5941 647 $data = new stdClass();
e0724506 648 $data->id = $existing->id;
9718765e 649 $data->value = $value;
650 $DB->update_record('grade_settings', $data);
e0724506 651 }
652
653 grade_get_setting($courseid, null, null, true); // reset the cache
654}
655
e9096dc2 656/**
657 * Returns string representation of grade value
ba21c9d4 658 *
a153c9f2
AD
659 * @param float $value The grade value
660 * @param object $grade_item Grade item object passed by reference to prevent scale reloading
e9096dc2 661 * @param bool $localized use localised decimal separator
a153c9f2
AD
662 * @param int $displaytype type of display. For example GRADE_DISPLAY_TYPE_REAL, GRADE_DISPLAY_TYPE_PERCENTAGE, GRADE_DISPLAY_TYPE_LETTER
663 * @param int $decimals The number of decimal places when displaying float values
e9096dc2 664 * @return string
665 */
666function grade_format_gradevalue($value, &$grade_item, $localized=true, $displaytype=null, $decimals=null) {
667 if ($grade_item->gradetype == GRADE_TYPE_NONE or $grade_item->gradetype == GRADE_TYPE_TEXT) {
668 return '';
669 }
670
671 // no grade yet?
672 if (is_null($value)) {
673 return '-';
674 }
675
676 if ($grade_item->gradetype != GRADE_TYPE_VALUE and $grade_item->gradetype != GRADE_TYPE_SCALE) {
677 //unknown type??
678 return '';
679 }
680
681 if (is_null($displaytype)) {
682 $displaytype = $grade_item->get_displaytype();
683 }
684
685 if (is_null($decimals)) {
686 $decimals = $grade_item->get_decimals();
687 }
688
689 switch ($displaytype) {
690 case GRADE_DISPLAY_TYPE_REAL:
7d10995c 691 return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized);
e9096dc2 692
693 case GRADE_DISPLAY_TYPE_PERCENTAGE:
7d10995c 694 return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized);
e9096dc2 695
696 case GRADE_DISPLAY_TYPE_LETTER:
7d10995c 697 return grade_format_gradevalue_letter($value, $grade_item);
e9096dc2 698
7d10995c 699 case GRADE_DISPLAY_TYPE_REAL_PERCENTAGE:
700 return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ' (' .
701 grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ')';
702
703 case GRADE_DISPLAY_TYPE_REAL_LETTER:
704 return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ' (' .
705 grade_format_gradevalue_letter($value, $grade_item) . ')';
706
707 case GRADE_DISPLAY_TYPE_PERCENTAGE_REAL:
708 return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ' (' .
709 grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ')';
710
711 case GRADE_DISPLAY_TYPE_LETTER_REAL:
712 return grade_format_gradevalue_letter($value, $grade_item) . ' (' .
713 grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ')';
e9096dc2 714
7d10995c 715 case GRADE_DISPLAY_TYPE_LETTER_PERCENTAGE:
716 return grade_format_gradevalue_letter($value, $grade_item) . ' (' .
717 grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ')';
718
719 case GRADE_DISPLAY_TYPE_PERCENTAGE_LETTER:
720 return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ' (' .
721 grade_format_gradevalue_letter($value, $grade_item) . ')';
e9096dc2 722 default:
723 return '';
724 }
725}
4b86bb08 726
ba21c9d4 727/**
a153c9f2
AD
728 * Returns a float representation of a grade value
729 *
730 * @param float $value The grade value
731 * @param object $grade_item Grade item object
732 * @param int $decimals The number of decimal places
733 * @param bool $localized use localised decimal separator
734 * @return string
ba21c9d4 735 */
7d10995c 736function grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) {
737 if ($grade_item->gradetype == GRADE_TYPE_SCALE) {
738 if (!$scale = $grade_item->load_scale()) {
739 return get_string('error');
740 }
741
653a8648 742 $value = $grade_item->bounded_grade($value);
7d10995c 743 return format_string($scale->scale_items[$value-1]);
744
745 } else {
746 return format_float($value, $decimals, $localized);
747 }
748}
a153c9f2 749
ba21c9d4 750/**
a153c9f2
AD
751 * Returns a percentage representation of a grade value
752 *
753 * @param float $value The grade value
754 * @param object $grade_item Grade item object
755 * @param int $decimals The number of decimal places
756 * @param bool $localized use localised decimal separator
757 * @return string
ba21c9d4 758 */
7d10995c 759function grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) {
760 $min = $grade_item->grademin;
761 $max = $grade_item->grademax;
762 if ($min == $max) {
763 return '';
764 }
653a8648 765 $value = $grade_item->bounded_grade($value);
7d10995c 766 $percentage = (($value-$min)*100)/($max-$min);
767 return format_float($percentage, $decimals, $localized).' %';
768}
a153c9f2 769
ba21c9d4 770/**
a153c9f2 771 * Returns a letter grade representation of a grade value
a4d76049 772 * The array of grade letters used is produced by {@link grade_get_letters()} using the course context
a153c9f2
AD
773 *
774 * @param float $value The grade value
775 * @param object $grade_item Grade item object
776 * @return string
ba21c9d4 777 */
7d10995c 778function grade_format_gradevalue_letter($value, $grade_item) {
b0c6dc1c 779 $context = context_course::instance($grade_item->courseid, IGNORE_MISSING);
7d10995c 780 if (!$letters = grade_get_letters($context)) {
781 return ''; // no letters??
782 }
783
653a8648 784 if (is_null($value)) {
785 return '-';
786 }
787
7d10995c 788 $value = grade_grade::standardise_score($value, $grade_item->grademin, $grade_item->grademax, 0, 100);
789 $value = bounded_number(0, $value, 100); // just in case
790 foreach ($letters as $boundary => $letter) {
791 if ($value >= $boundary) {
792 return format_string($letter);
793 }
794 }
795 return '-'; // no match? maybe '' would be more correct
796}
797
798
4b86bb08 799/**
a153c9f2 800 * Returns grade options for gradebook grade category menu
ba21c9d4 801 *
a153c9f2
AD
802 * @param int $courseid The course ID
803 * @param bool $includenew Include option for new category at array index -1
4b86bb08 804 * @return array of grade categories in course
805 */
806function grade_get_categories_menu($courseid, $includenew=false) {
807 $result = array();
808 if (!$categories = grade_category::fetch_all(array('courseid'=>$courseid))) {
809 //make sure course category exists
29c660c4 810 if (!grade_category::fetch_course_category($courseid)) {
4b86bb08 811 debugging('Can not create course grade category!');
812 return $result;
813 }
814 $categories = grade_category::fetch_all(array('courseid'=>$courseid));
815 }
816 foreach ($categories as $key=>$category) {
817 if ($category->is_course_category()) {
818 $result[$category->id] = get_string('uncategorised', 'grades');
819 unset($categories[$key]);
820 }
821 }
822 if ($includenew) {
823 $result[-1] = get_string('newcategory', 'grades');
824 }
825 $cats = array();
826 foreach ($categories as $category) {
827 $cats[$category->id] = $category->get_name();
828 }
2f1e464a 829 core_collator::asort($cats);
4b86bb08 830
831 return ($result+$cats);
832}
e9096dc2 833
834/**
a153c9f2 835 * Returns the array of grade letters to be used in the supplied context
ba21c9d4 836 *
a153c9f2
AD
837 * @param object $context Context object or null for defaults
838 * @return array of grade_boundary (minimum) => letter_string
e9096dc2 839 */
840function grade_get_letters($context=null) {
9718765e 841 global $DB;
842
e9096dc2 843 if (empty($context)) {
284abb09 844 //default grading letters
845 return array('93'=>'A', '90'=>'A-', '87'=>'B+', '83'=>'B', '80'=>'B-', '77'=>'C+', '73'=>'C', '70'=>'C-', '67'=>'D+', '60'=>'D', '0'=>'F');
e9096dc2 846 }
847
848 static $cache = array();
849
850 if (array_key_exists($context->id, $cache)) {
851 return $cache[$context->id];
852 }
853
854 if (count($cache) > 100) {
855 $cache = array(); // cache size limit
856 }
857
858 $letters = array();
859
8e8891b7 860 $contexts = $context->get_parent_context_ids();
e9096dc2 861 array_unshift($contexts, $context->id);
862
863 foreach ($contexts as $ctxid) {
9718765e 864 if ($records = $DB->get_records('grade_letters', array('contextid'=>$ctxid), 'lowerboundary DESC')) {
e9096dc2 865 foreach ($records as $record) {
284abb09 866 $letters[$record->lowerboundary] = $record->letter;
e9096dc2 867 }
868 }
869
870 if (!empty($letters)) {
871 $cache[$context->id] = $letters;
872 return $letters;
873 }
874 }
875
876 $letters = grade_get_letters(null);
877 $cache[$context->id] = $letters;
878 return $letters;
879}
880
60243313 881
882/**
a153c9f2 883 * Verify new value of grade item idnumber. Checks for uniqueness of new ID numbers. Old ID numbers are kept intact.
ba21c9d4 884 *
a153c9f2
AD
885 * @param string $idnumber string (with magic quotes)
886 * @param int $courseid ID numbers are course unique only
887 * @param grade_item $grade_item The grade item this idnumber is associated with
888 * @param stdClass $cm used for course module idnumbers and items attached to modules
889 * @return bool true means idnumber ok
60243313 890 */
204175c5 891function grade_verify_idnumber($idnumber, $courseid, $grade_item=null, $cm=null) {
9718765e 892 global $DB;
893
60243313 894 if ($idnumber == '') {
895 //we allow empty idnumbers
896 return true;
897 }
898
899 // keep existing even when not unique
900 if ($cm and $cm->idnumber == $idnumber) {
3d83539c
DM
901 if ($grade_item and $grade_item->itemnumber != 0) {
902 // grade item with itemnumber > 0 can't have the same idnumber as the main
903 // itemnumber 0 which is synced with course_modules
904 return false;
905 }
60243313 906 return true;
907 } else if ($grade_item and $grade_item->idnumber == $idnumber) {
908 return true;
909 }
910
9718765e 911 if ($DB->record_exists('course_modules', array('course'=>$courseid, 'idnumber'=>$idnumber))) {
60243313 912 return false;
913 }
914
9718765e 915 if ($DB->record_exists('grade_items', array('courseid'=>$courseid, 'idnumber'=>$idnumber))) {
60243313 916 return false;
917 }
918
919 return true;
920}
921
922/**
923 * Force final grade recalculation in all course items
ba21c9d4 924 *
a153c9f2 925 * @param int $courseid The course ID to recalculate
60243313 926 */
f8e6e4db 927function grade_force_full_regrading($courseid) {
9718765e 928 global $DB;
929 $DB->set_field('grade_items', 'needsupdate', 1, array('courseid'=>$courseid));
f8e6e4db 930}
34e67f76 931
653a8648 932/**
a153c9f2 933 * Forces regrading of all site grades. Used when changing site setings
653a8648 934 */
935function grade_force_site_regrading() {
936 global $CFG, $DB;
ae5a3939 937 $DB->set_field('grade_items', 'needsupdate', 1);
653a8648 938}
939
13ba9036
AD
940/**
941 * Recover a user's grades from grade_grades_history
942 * @param int $userid the user ID whose grades we want to recover
943 * @param int $courseid the relevant course
944 * @return bool true if successful or false if there was an error or no grades could be recovered
945 */
946function grade_recover_history_grades($userid, $courseid) {
947 global $CFG, $DB;
948
949 if ($CFG->disablegradehistory) {
950 debugging('Attempting to recover grades when grade history is disabled.');
951 return false;
952 }
953
954 //Were grades recovered? Flag to return.
955 $recoveredgrades = false;
956
957 //Check the user is enrolled in this course
958 //Dont bother checking if they have a gradeable role. They may get one later so recover
959 //whatever grades they have now just in case.
b0c6dc1c 960 $course_context = context_course::instance($courseid);
13ba9036
AD
961 if (!is_enrolled($course_context, $userid)) {
962 debugging('Attempting to recover the grades of a user who is deleted or not enrolled. Skipping recover.');
963 return false;
964 }
965
966 //Check for existing grades for this user in this course
967 //Recovering grades when the user already has grades can lead to duplicate indexes and bad data
968 //In the future we could move the existing grades to the history table then recover the grades from before then
2293a504
PS
969 $sql = "SELECT gg.id
970 FROM {grade_grades} gg
971 JOIN {grade_items} gi ON gi.id = gg.itemid
972 WHERE gi.courseid = :courseid AND gg.userid = :userid";
13ba9036
AD
973 $params = array('userid' => $userid, 'courseid' => $courseid);
974 if ($DB->record_exists_sql($sql, $params)) {
975 debugging('Attempting to recover the grades of a user who already has grades. Skipping recover.');
976 return false;
977 } else {
978 //Retrieve the user's old grades
979 //have history ID as first column to guarantee we a unique first column
2293a504
PS
980 $sql = "SELECT h.id, gi.itemtype, gi.itemmodule, gi.iteminstance as iteminstance, gi.itemnumber, h.source, h.itemid, h.userid, h.rawgrade, h.rawgrademax,
981 h.rawgrademin, h.rawscaleid, h.usermodified, h.finalgrade, h.hidden, h.locked, h.locktime, h.exported, h.overridden, h.excluded, h.feedback,
982 h.feedbackformat, h.information, h.informationformat, h.timemodified, itemcreated.tm AS timecreated
13ba9036 983 FROM {grade_grades_history} h
2293a504
PS
984 JOIN (SELECT itemid, MAX(id) AS id
985 FROM {grade_grades_history}
986 WHERE userid = :userid1
987 GROUP BY itemid) maxquery ON h.id = maxquery.id AND h.itemid = maxquery.itemid
13ba9036 988 JOIN {grade_items} gi ON gi.id = h.itemid
2293a504
PS
989 JOIN (SELECT itemid, MAX(timemodified) AS tm
990 FROM {grade_grades_history}
991 WHERE userid = :userid2 AND action = :insertaction
992 GROUP BY itemid) itemcreated ON itemcreated.itemid = h.itemid
13ba9036
AD
993 WHERE gi.courseid = :courseid";
994 $params = array('userid1' => $userid, 'userid2' => $userid , 'insertaction' => GRADE_HISTORY_INSERT, 'courseid' => $courseid);
995 $oldgrades = $DB->get_records_sql($sql, $params);
996
997 //now move the old grades to the grade_grades table
998 foreach ($oldgrades as $oldgrade) {
999 unset($oldgrade->id);
1000
1001 $grade = new grade_grade($oldgrade, false);//2nd arg false as dont want to try and retrieve a record from the DB
1002 $grade->insert($oldgrade->source);
1003
1004 //dont include default empty grades created when activities are created
2293a504 1005 if (!is_null($oldgrade->finalgrade) || !is_null($oldgrade->feedback)) {
13ba9036
AD
1006 $recoveredgrades = true;
1007 }
1008 }
1009 }
1010
1011 //Some activities require manual grade synching (moving grades from the activity into the gradebook)
1012 //If the student was deleted when synching was done they may have grades in the activity that haven't been moved across
1013 grade_grab_course_grades($courseid, null, $userid);
1014
1015 return $recoveredgrades;
1016}
1017
5834dcdb 1018/**
ac9b0805 1019 * Updates all final grades in course.
a8995b34 1020 *
a153c9f2
AD
1021 * @param int $courseid The course ID
1022 * @param int $userid If specified try to do a quick regrading of the grades of this user only
1023 * @param object $updated_item Optional grade item to be marked for regrading
1024 * @return bool true if ok, array of errors if problems found. Grade item id => error message
a8995b34 1025 */
c86caae7 1026function grade_regrade_final_grades($courseid, $userid=null, $updated_item=null) {
b8ff92b6 1027
514a3467 1028 $course_item = grade_item::fetch_course_item($courseid);
f04873a9 1029
f8e6e4db 1030 if ($userid) {
1031 // one raw grade updated for one user
1032 if (empty($updated_item)) {
2f137aa1 1033 print_error("cannotbenull", 'debug', '', "updated_item");
f8e6e4db 1034 }
1035 if ($course_item->needsupdate) {
1036 $updated_item->force_regrading();
b45d8391 1037 return array($course_item->id =>'Can not do fast regrading after updating of raw grades');
a8995b34 1038 }
772ddfbf 1039
f8e6e4db 1040 } else {
1041 if (!$course_item->needsupdate) {
1042 // nothing to do :-)
b8ff92b6 1043 return true;
b8ff92b6 1044 }
a8995b34 1045 }
1046
f8e6e4db 1047 $grade_items = grade_item::fetch_all(array('courseid'=>$courseid));
1048 $depends_on = array();
1049
1050 // first mark all category and calculated items as needing regrading
fb0e3570 1051 // this is slower, but 100% accurate
f8e6e4db 1052 foreach ($grade_items as $gid=>$gitem) {
fb46b5b6 1053 if (!empty($updated_item) and $updated_item->id == $gid) {
f8e6e4db 1054 $grade_items[$gid]->needsupdate = 1;
1055
eacd3700 1056 } else if ($gitem->is_course_item() or $gitem->is_category_item() or $gitem->is_calculated()) {
f8e6e4db 1057 $grade_items[$gid]->needsupdate = 1;
1058 }
2e53372c 1059
f8e6e4db 1060 // construct depends_on lookup array
1061 $depends_on[$gid] = $grade_items[$gid]->depends_on();
1062 }
2e53372c 1063
d14ae855 1064 $errors = array();
b8ff92b6 1065 $finalids = array();
d14ae855 1066 $gids = array_keys($grade_items);
eacd3700 1067 $failed = 0;
d14ae855 1068
1069 while (count($finalids) < count($gids)) { // work until all grades are final or error found
1070 $count = 0;
1071 foreach ($gids as $gid) {
1072 if (in_array($gid, $finalids)) {
1073 continue; // already final
1074 }
1075
1076 if (!$grade_items[$gid]->needsupdate) {
1077 $finalids[] = $gid; // we can make it final - does not need update
b8ff92b6 1078 continue;
1079 }
1080
b8ff92b6 1081 $doupdate = true;
f8e6e4db 1082 foreach ($depends_on[$gid] as $did) {
b8ff92b6 1083 if (!in_array($did, $finalids)) {
1084 $doupdate = false;
d14ae855 1085 continue; // this item depends on something that is not yet in finals array
b8ff92b6 1086 }
1087 }
1088
1089 //oki - let's update, calculate or aggregate :-)
1090 if ($doupdate) {
d14ae855 1091 $result = $grade_items[$gid]->regrade_final_grades($userid);
f8e6e4db 1092
1093 if ($result === true) {
d14ae855 1094 $grade_items[$gid]->regrading_finished();
fb0e3570 1095 $grade_items[$gid]->check_locktime(); // do the locktime item locking
f8e6e4db 1096 $count++;
b8ff92b6 1097 $finalids[] = $gid;
fb0e3570 1098
f8e6e4db 1099 } else {
d14ae855 1100 $grade_items[$gid]->force_regrading();
f8e6e4db 1101 $errors[$gid] = $result;
b8ff92b6 1102 }
1103 }
1104 }
1105
1106 if ($count == 0) {
eacd3700 1107 $failed++;
1108 } else {
1109 $failed = 0;
1110 }
1111
1112 if ($failed > 1) {
d14ae855 1113 foreach($gids as $gid) {
1114 if (in_array($gid, $finalids)) {
1115 continue; // this one is ok
1116 }
1117 $grade_items[$gid]->force_regrading();
459843d4 1118 $errors[$grade_items[$gid]->id] = get_string('errorcalculationbroken', 'grades');
b8ff92b6 1119 }
459843d4 1120 break; // Found error.
b8ff92b6 1121 }
1122 }
1123
1124 if (count($errors) == 0) {
fb0e3570 1125 if (empty($userid)) {
1126 // do the locktime locking of grades, but only when doing full regrading
fed7cdc9 1127 grade_grade::check_locktime_all($gids);
fb0e3570 1128 }
b8ff92b6 1129 return true;
1130 } else {
1131 return $errors;
1132 }
a8995b34 1133}
967f222f 1134
ac9b0805 1135/**
a153c9f2
AD
1136 * Refetches grade data from course activities
1137 *
1138 * @param int $courseid The course ID
1139 * @param string $modname Limit the grade fetch to a single module type. For example 'forum'
13ba9036 1140 * @param int $userid limit the grade fetch to a single user
ac9b0805 1141 */
13ba9036 1142function grade_grab_course_grades($courseid, $modname=null, $userid=0) {
9718765e 1143 global $CFG, $DB;
ac9b0805 1144
f0362b5d 1145 if ($modname) {
1146 $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname
9718765e 1147 FROM {".$modname."} a, {course_modules} cm, {modules} m
1148 WHERE m.name=:modname AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=:courseid";
1149 $params = array('modname'=>$modname, 'courseid'=>$courseid);
f0362b5d 1150
9718765e 1151 if ($modinstances = $DB->get_records_sql($sql, $params)) {
f0362b5d 1152 foreach ($modinstances as $modinstance) {
13ba9036 1153 grade_update_mod_grades($modinstance, $userid);
f0362b5d 1154 }
1155 }
1156 return;
1157 }
1158
bd3b3bba 1159 if (!$mods = core_component::get_plugin_list('mod') ) {
2f137aa1 1160 print_error('nomodules', 'debug');
ac9b0805 1161 }
1162
17da2e6f 1163 foreach ($mods as $mod => $fullmod) {
ac9b0805 1164 if ($mod == 'NEWMODULE') { // Someone has unzipped the template, ignore it
1165 continue;
1166 }
1167
ac9b0805 1168 // include the module lib once
1169 if (file_exists($fullmod.'/lib.php')) {
f0362b5d 1170 // get all instance of the activity
1171 $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname
9718765e 1172 FROM {".$mod."} a, {course_modules} cm, {modules} m
1173 WHERE m.name=:mod AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=:courseid";
1174 $params = array('mod'=>$mod, 'courseid'=>$courseid);
f0362b5d 1175
9718765e 1176 if ($modinstances = $DB->get_records_sql($sql, $params)) {
f0362b5d 1177 foreach ($modinstances as $modinstance) {
13ba9036 1178 grade_update_mod_grades($modinstance, $userid);
f0362b5d 1179 }
ac9b0805 1180 }
1181 }
1182 }
1183}
1184
d185c3ee 1185/**
398a160d 1186 * Force full update of module grades in central gradebook
ba21c9d4 1187 *
a153c9f2
AD
1188 * @param object $modinstance Module object with extra cmidnumber and modname property
1189 * @param int $userid Optional user ID if limiting the update to a single user
1190 * @return bool True if success
d185c3ee 1191 */
2b0f65e2 1192function grade_update_mod_grades($modinstance, $userid=0) {
9718765e 1193 global $CFG, $DB;
d185c3ee 1194
1195 $fullmod = $CFG->dirroot.'/mod/'.$modinstance->modname;
1196 if (!file_exists($fullmod.'/lib.php')) {
653a8648 1197 debugging('missing lib.php file in module ' . $modinstance->modname);
d185c3ee 1198 return false;
1199 }
1200 include_once($fullmod.'/lib.php');
1201
d185c3ee 1202 $updateitemfunc = $modinstance->modname.'_grade_item_update';
a153c9f2 1203 $updategradesfunc = $modinstance->modname.'_update_grades';
d185c3ee 1204
398a160d 1205 if (function_exists($updategradesfunc) and function_exists($updateitemfunc)) {
d185c3ee 1206 //new grading supported, force updating of grades
1207 $updateitemfunc($modinstance);
2b0f65e2 1208 $updategradesfunc($modinstance, $userid);
d185c3ee 1209
1210 } else {
69bcca5e 1211 // Module does not support grading?
d185c3ee 1212 }
1213
1214 return true;
1215}
de420c11 1216
b51ece5b 1217/**
1218 * Remove grade letters for given context
ba21c9d4 1219 *
a153c9f2
AD
1220 * @param context $context The context
1221 * @param bool $showfeedback If true a success notification will be displayed
b51ece5b 1222 */
1223function remove_grade_letters($context, $showfeedback) {
aa9a6867 1224 global $DB, $OUTPUT;
9718765e 1225
b51ece5b 1226 $strdeleted = get_string('deleted');
1227
9718765e 1228 $DB->delete_records('grade_letters', array('contextid'=>$context->id));
b51ece5b 1229 if ($showfeedback) {
16ef46e7 1230 echo $OUTPUT->notification($strdeleted.' - '.get_string('letters', 'grades'), 'notifysuccess');
b51ece5b 1231 }
1232}
16ef46e7 1233
f615fbab 1234/**
a153c9f2
AD
1235 * Remove all grade related course data
1236 * Grade history is kept
ba21c9d4 1237 *
a153c9f2
AD
1238 * @param int $courseid The course ID
1239 * @param bool $showfeedback If true success notifications will be displayed
f615fbab 1240 */
1241function remove_course_grades($courseid, $showfeedback) {
aa9a6867 1242 global $DB, $OUTPUT;
9718765e 1243
16ef46e7 1244 $fs = get_file_storage();
f615fbab 1245 $strdeleted = get_string('deleted');
1246
1247 $course_category = grade_category::fetch_course_category($courseid);
1248 $course_category->delete('coursedelete');
b0c6dc1c 1249 $fs->delete_area_files(context_course::instance($courseid)->id, 'grade', 'feedback');
f615fbab 1250 if ($showfeedback) {
16ef46e7 1251 echo $OUTPUT->notification($strdeleted.' - '.get_string('grades', 'grades').', '.get_string('items', 'grades').', '.get_string('categories', 'grades'), 'notifysuccess');
f615fbab 1252 }
1253
1254 if ($outcomes = grade_outcome::fetch_all(array('courseid'=>$courseid))) {
1255 foreach ($outcomes as $outcome) {
1256 $outcome->delete('coursedelete');
1257 }
1258 }
9718765e 1259 $DB->delete_records('grade_outcomes_courses', array('courseid'=>$courseid));
f615fbab 1260 if ($showfeedback) {
16ef46e7 1261 echo $OUTPUT->notification($strdeleted.' - '.get_string('outcomes', 'grades'), 'notifysuccess');
f615fbab 1262 }
1263
1264 if ($scales = grade_scale::fetch_all(array('courseid'=>$courseid))) {
1265 foreach ($scales as $scale) {
1266 $scale->delete('coursedelete');
1267 }
1268 }
1269 if ($showfeedback) {
16ef46e7 1270 echo $OUTPUT->notification($strdeleted.' - '.get_string('scales'), 'notifysuccess');
f615fbab 1271 }
b51ece5b 1272
9718765e 1273 $DB->delete_records('grade_settings', array('courseid'=>$courseid));
b51ece5b 1274 if ($showfeedback) {
16ef46e7 1275 echo $OUTPUT->notification($strdeleted.' - '.get_string('settings', 'grades'), 'notifysuccess');
b51ece5b 1276 }
f615fbab 1277}
bfe7297e 1278
e2b347e9 1279/**
a153c9f2
AD
1280 * Called when course category is deleted
1281 * Cleans the gradebook of associated data
ba21c9d4 1282 *
a153c9f2
AD
1283 * @param int $categoryid The course category id
1284 * @param int $newparentid If empty everything is deleted. Otherwise the ID of the category where content moved
e2b347e9 1285 * @param bool $showfeedback print feedback
1286 */
1287function grade_course_category_delete($categoryid, $newparentid, $showfeedback) {
9718765e 1288 global $DB;
1289
b0c6dc1c 1290 $context = context_coursecat::instance($categoryid);
9718765e 1291 $DB->delete_records('grade_letters', array('contextid'=>$context->id));
e2b347e9 1292}
1293
8a0a6046 1294/**
a153c9f2
AD
1295 * Does gradebook cleanup when a module is uninstalled
1296 * Deletes all associated grade items
ba21c9d4 1297 *
a153c9f2 1298 * @param string $modname The grade item module name to remove. For example 'forum'
8a0a6046 1299 */
1300function grade_uninstalled_module($modname) {
9718765e 1301 global $CFG, $DB;
8a0a6046 1302
1303 $sql = "SELECT *
9718765e 1304 FROM {grade_items}
1305 WHERE itemtype='mod' AND itemmodule=?";
8a0a6046 1306
1307 // go all items for this module and delete them including the grades
b967c541
EL
1308 $rs = $DB->get_recordset_sql($sql, array($modname));
1309 foreach ($rs as $item) {
1310 $grade_item = new grade_item($item, false);
1311 $grade_item->delete('moduninstall');
8a0a6046 1312 }
b967c541 1313 $rs->close();
8a0a6046 1314}
1315
df997f84 1316/**
a153c9f2
AD
1317 * Deletes all of a user's grade data from gradebook
1318 *
1319 * @param int $userid The user whose grade data should be deleted
df997f84
PS
1320 */
1321function grade_user_delete($userid) {
1322 if ($grades = grade_grade::fetch_all(array('userid'=>$userid))) {
1323 foreach ($grades as $grade) {
1324 $grade->delete('userdelete');
1325 }
1326 }
1327}
1328
1329/**
a153c9f2
AD
1330 * Purge course data when user unenrolls from a course
1331 *
1332 * @param int $courseid The ID of the course the user has unenrolled from
1333 * @param int $userid The ID of the user unenrolling
df997f84
PS
1334 */
1335function grade_user_unenrol($courseid, $userid) {
1336 if ($items = grade_item::fetch_all(array('courseid'=>$courseid))) {
1337 foreach ($items as $item) {
1338 if ($grades = grade_grade::fetch_all(array('userid'=>$userid, 'itemid'=>$item->id))) {
1339 foreach ($grades as $grade) {
1340 $grade->delete('userdelete');
1341 }
1342 }
1343 }
1344 }
1345}
1346
2650c51e 1347/**
a153c9f2 1348 * Grading cron job. Performs background clean up on the gradebook
2650c51e 1349 */
1350function grade_cron() {
9718765e 1351 global $CFG, $DB;
26101be8 1352
1353 $now = time();
1354
1355 $sql = "SELECT i.*
9718765e 1356 FROM {grade_items} i
1357 WHERE i.locked = 0 AND i.locktime > 0 AND i.locktime < ? AND EXISTS (
1358 SELECT 'x' FROM {grade_items} c WHERE c.itemtype='course' AND c.needsupdate=0 AND c.courseid=i.courseid)";
26101be8 1359
2650c51e 1360 // go through all courses that have proper final grades and lock them if needed
b967c541
EL
1361 $rs = $DB->get_recordset_sql($sql, array($now));
1362 foreach ($rs as $item) {
1363 $grade_item = new grade_item($item, false);
1364 $grade_item->locked = $now;
1365 $grade_item->update('locktime');
2650c51e 1366 }
b967c541 1367 $rs->close();
26101be8 1368
fcac8e51 1369 $grade_inst = new grade_grade();
1370 $fields = 'g.'.implode(',g.', $grade_inst->required_fields);
1371
1372 $sql = "SELECT $fields
9718765e 1373 FROM {grade_grades} g, {grade_items} i
1374 WHERE g.locked = 0 AND g.locktime > 0 AND g.locktime < ? AND g.itemid=i.id AND EXISTS (
1375 SELECT 'x' FROM {grade_items} c WHERE c.itemtype='course' AND c.needsupdate=0 AND c.courseid=i.courseid)";
26101be8 1376
1377 // go through all courses that have proper final grades and lock them if needed
b967c541
EL
1378 $rs = $DB->get_recordset_sql($sql, array($now));
1379 foreach ($rs as $grade) {
1380 $grade_grade = new grade_grade($grade, false);
1381 $grade_grade->locked = $now;
1382 $grade_grade->update('locktime');
26101be8 1383 }
b967c541 1384 $rs->close();
26101be8 1385
1ee0df06 1386 //TODO: do not run this cleanup every cron invocation
1387 // cleanup history tables
f0362b5d 1388 if (!empty($CFG->gradehistorylifetime)) { // value in days
1389 $histlifetime = $now - ($CFG->gradehistorylifetime * 3600 * 24);
1390 $tables = array('grade_outcomes_history', 'grade_categories_history', 'grade_items_history', 'grade_grades_history', 'scale_history');
1391 foreach ($tables as $table) {
9718765e 1392 if ($DB->delete_records_select($table, "timemodified < ?", array($histlifetime))) {
f0362b5d 1393 mtrace(" Deleted old grade history records from '$table'");
1ee0df06 1394 }
1395 }
f0362b5d 1396 }
1397}
1398
1399/**
a153c9f2 1400 * Reset all course grades, refetch from the activities and recalculate
ba21c9d4 1401 *
a153c9f2 1402 * @param int $courseid The course to reset
ba21c9d4 1403 * @return bool success
f0362b5d 1404 */
1405function grade_course_reset($courseid) {
1406
1407 // no recalculations
1408 grade_force_full_regrading($courseid);
1409
1410 $grade_items = grade_item::fetch_all(array('courseid'=>$courseid));
1411 foreach ($grade_items as $gid=>$grade_item) {
1412 $grade_item->delete_all_grades('reset');
1413 }
1ee0df06 1414
f0362b5d 1415 //refetch all grades
1416 grade_grab_course_grades($courseid);
1ee0df06 1417
f0362b5d 1418 // recalculate all grades
1419 grade_regrade_final_grades($courseid);
1420 return true;
2650c51e 1421}
1422
b45d8391 1423/**
a153c9f2 1424 * Convert a number to 5 decimal point float, an empty string or a null db compatible format
66690b69 1425 * (we need this to decide if db value changed)
ba21c9d4 1426 *
a153c9f2 1427 * @param mixed $number The number to convert
b45d8391 1428 * @return mixed float or null
1429 */
1430function grade_floatval($number) {
66690b69 1431 if (is_null($number) or $number === '') {
b45d8391 1432 return null;
1433 }
66690b69 1434 // we must round to 5 digits to get the same precision as in 10,5 db fields
25bcd908 1435 // note: db rounding for 10,5 is different from php round() function
66690b69 1436 return round($number, 5);
b45d8391 1437}
25bcd908 1438
1439/**
a4d76049 1440 * Compare two float numbers safely. Uses 5 decimals php precision using {@link grade_floatval()}. Nulls accepted too.
a153c9f2 1441 * Used for determining if a database update is required
ba21c9d4 1442 *
a153c9f2
AD
1443 * @param float $f1 Float one to compare
1444 * @param float $f2 Float two to compare
1445 * @return bool True if the supplied values are different
25bcd908 1446 */
1447function grade_floats_different($f1, $f2) {
1448 // note: db rounding for 10,5 is different from php round() function
1449 return (grade_floatval($f1) !== grade_floatval($f2));
1450}
1451
f162c15a 1452/**
a4d76049 1453 * Compare two float numbers safely. Uses 5 decimals php precision using {@link grade_floatval()}
f162c15a 1454 *
1455 * Do not use rounding for 10,5 at the database level as the results may be
1456 * different from php round() function.
1457 *
1458 * @since 2.0
a153c9f2
AD
1459 * @param float $f1 Float one to compare
1460 * @param float $f2 Float two to compare
1461 * @return bool True if the values should be considered as the same grades
f162c15a 1462 */
1463function grade_floats_equal($f1, $f2) {
1464 return (grade_floatval($f1) === grade_floatval($f2));
1465}