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