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