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