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