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