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