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