MDL-13375 reverting, the bug is somewhere else
[moodle.git] / lib / gradelib.php
CommitLineData
5834dcdb 1<?php // $Id$
2
3///////////////////////////////////////////////////////////////////////////
5834dcdb 4// NOTICE OF COPYRIGHT //
5// //
6// Moodle - Modular Object-Oriented Dynamic Learning Environment //
53461661 7// http://moodle.org //
5834dcdb 8// //
53461661 9// Copyright (C) 1999 onwards Martin Dougiamas http://moodle.com //
5834dcdb 10// //
11// This program is free software; you can redistribute it and/or modify //
12// it under the terms of the GNU General Public License as published by //
13// the Free Software Foundation; either version 2 of the License, or //
14// (at your option) any later version. //
15// //
16// This program is distributed in the hope that it will be useful, //
17// but WITHOUT ANY WARRANTY; without even the implied warranty of //
18// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
19// GNU General Public License for more details: //
20// //
21// http://www.gnu.org/copyleft/gpl.html //
22// //
23///////////////////////////////////////////////////////////////////////////
24
25/**
b9f49659 26 * Library of functions for gradebook - both public and internal
5834dcdb 27 *
28 * @author Moodle HQ developers
d886a7ea 29 * @version $Id$
5834dcdb 30 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
31 * @package moodlecore
32 */
33
53461661 34require_once($CFG->libdir . '/grade/constants.php');
eea6690a 35
3058964f 36require_once($CFG->libdir . '/grade/grade_category.php');
37require_once($CFG->libdir . '/grade/grade_item.php');
3ee5c201 38require_once($CFG->libdir . '/grade/grade_grade.php');
d5bdb228 39require_once($CFG->libdir . '/grade/grade_scale.php');
5501446d 40require_once($CFG->libdir . '/grade/grade_outcome.php');
60cf7430 41
b9f49659 42/////////////////////////////////////////////////////////////////////
43///// Start of public API for communication with modules/blocks /////
44/////////////////////////////////////////////////////////////////////
612607bd 45
c5b5f18d 46/**
47 * Submit new or update grade; update/create grade_item definition. Grade must have userid specified,
ac9b0805 48 * rawgrade and feedback with format are optional. rawgrade NULL means 'Not graded', missing property
c5b5f18d 49 * or key means do not change existing.
4cf1b9be 50 *
c5b5f18d 51 * Only following grade item properties can be changed 'itemname', 'idnumber', 'gradetype', 'grademax',
f0362b5d 52 * 'grademin', 'scaleid', 'multfactor', 'plusfactor', 'deleted' and 'hidden'. 'reset' means delete all current grades including locked ones.
4cf1b9be 53 *
fcac8e51 54 * Manual, course or category items can not be updated by this function.
b9f49659 55 * @public
9c8d38fa 56 * @param string $source source of the grade such as 'mod/assignment'
c5b5f18d 57 * @param int $courseid id of course
3a5ae660 58 * @param string $itemtype type of grade item - mod, block
c5b5f18d 59 * @param string $itemmodule more specific then $itemtype - assignment, forum, etc.; maybe NULL for some item types
60 * @param int $iteminstance instance it of graded subject
61 * @param int $itemnumber most probably 0, modules can use other numbers when having more than one grades for each user
b60b2ce6 62 * @param mixed $grades grade (object, array) or several grades (arrays of arrays or objects), NULL if updating grade_item definition only
c5b5f18d 63 * @param mixed $itemdetails object or array describing the grading item, NULL if no change
64 */
b67ec72f 65function grade_update($source, $courseid, $itemtype, $itemmodule, $iteminstance, $itemnumber, $grades=NULL, $itemdetails=NULL) {
aaff71da 66 global $USER;
612607bd 67
c5b5f18d 68 // only following grade_item properties can be changed in this function
1223d24a 69 $allowed = array('itemname', 'idnumber', 'gradetype', 'grademax', 'grademin', 'scaleid', 'multfactor', 'plusfactor', 'deleted', 'hidden');
612607bd 70
c4e4068f 71 // grade item identification
72 $params = compact('courseid', 'itemtype', 'itemmodule', 'iteminstance', 'itemnumber');
73
612607bd 74 if (is_null($courseid) or is_null($itemtype)) {
75 debugging('Missing courseid or itemtype');
76 return GRADE_UPDATE_FAILED;
77 }
78
c4e4068f 79 if (!$grade_items = grade_item::fetch_all($params)) {
612607bd 80 // create a new one
81 $grade_item = false;
82
83 } else if (count($grade_items) == 1){
84 $grade_item = reset($grade_items);
85 unset($grade_items); //release memory
86
87 } else {
34e67f76 88 debugging('Found more than one grade item');
612607bd 89 return GRADE_UPDATE_MULTIPLE;
90 }
91
aaff71da 92 if (!empty($itemdetails['deleted'])) {
93 if ($grade_item) {
94 if ($grade_item->delete($source)) {
95 return GRADE_UPDATE_OK;
96 } else {
97 return GRADE_UPDATE_FAILED;
98 }
99 }
100 return GRADE_UPDATE_OK;
101 }
102
612607bd 103/// Create or update the grade_item if needed
b159da78 104
612607bd 105 if (!$grade_item) {
612607bd 106 if ($itemdetails) {
107 $itemdetails = (array)$itemdetails;
2e53372c 108
772ddfbf 109 // grademin and grademax ignored when scale specified
2e53372c 110 if (array_key_exists('scaleid', $itemdetails)) {
111 if ($itemdetails['scaleid']) {
112 unset($itemdetails['grademin']);
113 unset($itemdetails['grademax']);
114 }
115 }
116
612607bd 117 foreach ($itemdetails as $k=>$v) {
118 if (!in_array($k, $allowed)) {
119 // ignore it
120 continue;
121 }
122 if ($k == 'gradetype' and $v == GRADE_TYPE_NONE) {
123 // no grade item needed!
124 return GRADE_UPDATE_OK;
125 }
126 $params[$k] = $v;
127 }
128 }
f70152b7 129 $grade_item = new grade_item($params);
130 $grade_item->insert();
612607bd 131
132 } else {
2cc4b0f9 133 if ($grade_item->is_locked()) {
d6bc2a81 134 // no notice() here, test returned value instead!
678e8898 135 return GRADE_UPDATE_ITEM_LOCKED;
612607bd 136 }
137
138 if ($itemdetails) {
139 $itemdetails = (array)$itemdetails;
140 $update = false;
141 foreach ($itemdetails as $k=>$v) {
142 if (!in_array($k, $allowed)) {
143 // ignore it
144 continue;
145 }
146 if ($grade_item->{$k} != $v) {
147 $grade_item->{$k} = $v;
148 $update = true;
149 }
150 }
151 if ($update) {
152 $grade_item->update();
153 }
154 }
155 }
156
f0362b5d 157/// reset grades if requested
158 if (!empty($itemdetails['reset'])) {
159 $grade_item->delete_all_grades('reset');
160 return GRADE_UPDATE_OK;
161 }
162
612607bd 163/// Some extra checks
164 // do we use grading?
165 if ($grade_item->gradetype == GRADE_TYPE_NONE) {
166 return GRADE_UPDATE_OK;
167 }
168
169 // no grade submitted
b67ec72f 170 if (empty($grades)) {
612607bd 171 return GRADE_UPDATE_OK;
172 }
173
612607bd 174/// Finally start processing of grades
b67ec72f 175 if (is_object($grades)) {
176 $grades = array($grades);
612607bd 177 } else {
b67ec72f 178 if (array_key_exists('userid', $grades)) {
179 $grades = array($grades);
612607bd 180 }
181 }
182
4cf1b9be 183 $failed = false;
612607bd 184 foreach ($grades as $grade) {
185 $grade = (array)$grade;
186 if (empty($grade['userid'])) {
4cf1b9be 187 $failed = true;
188 debugging('Invalid userid in grade submitted');
189 continue;
ac9b0805 190 } else {
191 $userid = $grade['userid'];
612607bd 192 }
193
2cc4b0f9 194 $rawgrade = false;
ac9b0805 195 $feedback = false;
196 $feedbackformat = FORMAT_MOODLE;
ced5ee59 197 $usermodified = $USER->id;
198 $datesubmitted = null;
199 $dategraded = null;
772ddfbf 200
ac9b0805 201 if (array_key_exists('rawgrade', $grade)) {
202 $rawgrade = $grade['rawgrade'];
203 }
612607bd 204
4cf1b9be 205 if (array_key_exists('feedback', $grade)) {
ac9b0805 206 $feedback = $grade['feedback'];
612607bd 207 }
208
ac9b0805 209 if (array_key_exists('feedbackformat', $grade)) {
210 $feedbackformat = $grade['feedbackformat'];
612607bd 211 }
212
aaff71da 213 if (array_key_exists('usermodified', $grade)) {
214 $usermodified = $grade['usermodified'];
ced5ee59 215 }
216
217 if (array_key_exists('datesubmitted', $grade)) {
218 $datesubmitted = $grade['datesubmitted'];
219 }
220
221 if (array_key_exists('dategraded', $grade)) {
222 $dategraded = $grade['dategraded'];
aaff71da 223 }
224
ac9b0805 225 // update or insert the grade
ced5ee59 226 if (!$grade_item->update_raw_grade($userid, $rawgrade, $source, $feedback, $feedbackformat, $usermodified, $dategraded, $datesubmitted)) {
4cf1b9be 227 $failed = true;
4cf1b9be 228 }
612607bd 229 }
230
4cf1b9be 231 if (!$failed) {
232 return GRADE_UPDATE_OK;
233 } else {
234 return GRADE_UPDATE_FAILED;
235 }
612607bd 236}
237
3a5ae660 238/**
239 * Updates outcomes of user
fcac8e51 240 * Manual outcomes can not be updated.
b9f49659 241 * @public
fcac8e51 242 * @param string $source source of the grade such as 'mod/assignment'
3a5ae660 243 * @param int $courseid id of course
244 * @param string $itemtype 'mod', 'block'
245 * @param string $itemmodule 'forum, 'quiz', etc.
246 * @param int $iteminstance id of the item module
247 * @param int $userid ID of the graded user
d886a7ea 248 * @param array $data array itemnumber=>outcomegrade
3a5ae660 249 */
250function grade_update_outcomes($source, $courseid, $itemtype, $itemmodule, $iteminstance, $userid, $data) {
251 if ($items = grade_item::fetch_all(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid))) {
252 foreach ($items as $item) {
d886a7ea 253 if (!array_key_exists($item->itemnumber, $data)) {
3a5ae660 254 continue;
255 }
d886a7ea 256 $grade = $data[$item->itemnumber] < 1 ? null : $data[$item->itemnumber];
3a5ae660 257 $item->update_final_grade($userid, $grade, $source);
11a14999 258 }
3a5ae660 259 }
260}
261
6b5c722d 262/**
fcac8e51 263 * Returns grading information for given activity - optionally with users grades
264 * Manual, course or category items can not be queried.
b9f49659 265 * @public
6b5c722d 266 * @param int $courseid id of course
267 * @param string $itemtype 'mod', 'block'
268 * @param string $itemmodule 'forum, 'quiz', etc.
269 * @param int $iteminstance id of the item module
b9f49659 270 * @param int $userid_or_ids optional id of the graded user or array of ids; if userid not used, returns only information about grade_item
6b5c722d 271 * @return array of grade information objects (scaleid, name, grade and locked status, etc.) indexed with itemnumbers
272 */
b9f49659 273function grade_get_grades($courseid, $itemtype, $itemmodule, $iteminstance, $userid_or_ids=null) {
a3fbd494 274 global $CFG;
275
fcac8e51 276 $return = new object();
277 $return->items = array();
278 $return->outcomes = array();
6b5c722d 279
fcac8e51 280 $course_item = grade_item::fetch_course_item($courseid);
281 $needsupdate = array();
282 if ($course_item->needsupdate) {
283 $result = grade_regrade_final_grades($courseid);
284 if ($result !== true) {
285 $needsupdate = array_keys($result);
286 }
287 }
6b5c722d 288
fcac8e51 289 if ($grade_items = grade_item::fetch_all(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid))) {
290 foreach ($grade_items as $grade_item) {
a3fbd494 291 $decimalpoints = null;
292
fcac8e51 293 if (empty($grade_item->outcomeid)) {
294 // prepare information about grade item
295 $item = new object();
296 $item->itemnumber = $grade_item->itemnumber;
297 $item->scaleid = $grade_item->scaleid;
298 $item->name = $grade_item->get_name();
299 $item->grademin = $grade_item->grademin;
300 $item->grademax = $grade_item->grademax;
301 $item->gradepass = $grade_item->gradepass;
302 $item->locked = $grade_item->is_locked();
303 $item->hidden = $grade_item->is_hidden();
304 $item->grades = array();
305
306 switch ($grade_item->gradetype) {
307 case GRADE_TYPE_NONE:
308 continue;
6b5c722d 309
6b5c722d 310 case GRADE_TYPE_VALUE:
fcac8e51 311 $item->scaleid = 0;
6b5c722d 312 break;
313
fcac8e51 314 case GRADE_TYPE_TEXT:
315 $item->scaleid = 0;
316 $item->grademin = 0;
317 $item->grademax = 0;
318 $item->gradepass = 0;
6b5c722d 319 break;
fcac8e51 320 }
6b5c722d 321
fcac8e51 322 if (empty($userid_or_ids)) {
323 $userids = array();
324
325 } else if (is_array($userid_or_ids)) {
326 $userids = $userid_or_ids;
327
328 } else {
329 $userids = array($userid_or_ids);
6b5c722d 330 }
6b5c722d 331
fcac8e51 332 if ($userids) {
333 $grade_grades = grade_grade::fetch_users_grades($grade_item, $userids, true);
334 foreach ($userids as $userid) {
335 $grade_grades[$userid]->grade_item =& $grade_item;
336
337 $grade = new object();
338 $grade->grade = $grade_grades[$userid]->finalgrade;
339 $grade->locked = $grade_grades[$userid]->is_locked();
340 $grade->hidden = $grade_grades[$userid]->is_hidden();
341 $grade->overridden = $grade_grades[$userid]->overridden;
342 $grade->feedback = $grade_grades[$userid]->feedback;
343 $grade->feedbackformat = $grade_grades[$userid]->feedbackformat;
a3fbd494 344 $grade->usermodified = $grade_grades[$userid]->usermodified;
ced5ee59 345 $grade->datesubmitted = $grade_grades[$userid]->get_datesubmitted();
346 $grade->dategraded = $grade_grades[$userid]->get_dategraded();
fcac8e51 347
348 // create text representation of grade
349 if (in_array($grade_item->id, $needsupdate)) {
85a0a69f 350 $grade->grade = false;
351 $grade->str_grade = get_string('error');
352 $grade->str_long_grade = $grade->str_grade;
fcac8e51 353
354 } else if (is_null($grade->grade)) {
85a0a69f 355 $grade->str_grade = '-';
356 $grade->str_long_grade = $grade->str_grade;
fcac8e51 357
358 } else {
e9096dc2 359 $grade->str_grade = grade_format_gradevalue($grade->grade, $grade_item);
85a0a69f 360 if ($grade_item->gradetype == GRADE_TYPE_SCALE or $grade_item->get_displaytype() != GRADE_DISPLAY_TYPE_REAL) {
361 $grade->str_long_grade = $grade->str_grade;
362 } else {
363 $a = new object();
364 $a->grade = $grade->str_grade;
365 $a->max = grade_format_gradevalue($grade_item->grademax, $grade_item);
366 $grade->str_long_grade = get_string('gradelong', 'grades', $a);
367 }
fcac8e51 368 }
369
370 // create html representation of feedback
371 if (is_null($grade->feedback)) {
372 $grade->str_feedback = '';
373 } else {
374 $grade->str_feedback = format_text($grade->feedback, $grade->feedbackformat);
375 }
376
377 $item->grades[$userid] = $grade;
378 }
379 }
380 $return->items[$grade_item->itemnumber] = $item;
381
6b5c722d 382 } else {
fcac8e51 383 if (!$grade_outcome = grade_outcome::fetch(array('id'=>$grade_item->outcomeid))) {
384 debugging('Incorect outcomeid found');
385 continue;
386 }
387
388 // outcome info
389 $outcome = new object();
390 $outcome->itemnumber = $grade_item->itemnumber;
391 $outcome->scaleid = $grade_outcome->scaleid;
392 $outcome->name = $grade_outcome->get_name();
393 $outcome->locked = $grade_item->is_locked();
394 $outcome->hidden = $grade_item->is_hidden();
395
396 if (empty($userid_or_ids)) {
397 $userids = array();
398 } else if (is_array($userid_or_ids)) {
399 $userids = $userid_or_ids;
400 } else {
401 $userids = array($userid_or_ids);
402 }
6b5c722d 403
fcac8e51 404 if ($userids) {
405 $grade_grades = grade_grade::fetch_users_grades($grade_item, $userids, true);
406 foreach ($userids as $userid) {
407 $grade_grades[$userid]->grade_item =& $grade_item;
408
409 $grade = new object();
410 $grade->grade = $grade_grades[$userid]->finalgrade;
411 $grade->locked = $grade_grades[$userid]->is_locked();
412 $grade->hidden = $grade_grades[$userid]->is_hidden();
413 $grade->feedback = $grade_grades[$userid]->feedback;
414 $grade->feedbackformat = $grade_grades[$userid]->feedbackformat;
a3fbd494 415 $grade->usermodified = $grade_grades[$userid]->usermodified;
fcac8e51 416
417 // create text representation of grade
418 if (in_array($grade_item->id, $needsupdate)) {
419 $grade->grade = false;
420 $grade->str_grade = get_string('error');
421
422 } else if (is_null($grade->grade)) {
423 $grade->grade = 0;
424 $grade->str_grade = get_string('nooutcome', 'grades');
425
426 } else {
427 $grade->grade = (int)$grade->grade;
428 $scale = $grade_item->load_scale();
429 $grade->str_grade = format_string($scale->scale_items[(int)$grade->grade-1]);
430 }
431
432 // create html representation of feedback
433 if (is_null($grade->feedback)) {
434 $grade->str_feedback = '';
435 } else {
436 $grade->str_feedback = format_text($grade->feedback, $grade->feedbackformat);
437 }
438
439 $outcome->grades[$userid] = $grade;
440 }
441 }
d886a7ea 442 $return->outcomes[$grade_item->itemnumber] = $outcome;
fcac8e51 443
444 }
6b5c722d 445 }
446 }
447
fcac8e51 448 // sort results using itemnumbers
449 ksort($return->items, SORT_NUMERIC);
450 ksort($return->outcomes, SORT_NUMERIC);
451
452 return $return;
6b5c722d 453}
454
b9f49659 455///////////////////////////////////////////////////////////////////
456///// End of public API for communication with modules/blocks /////
457///////////////////////////////////////////////////////////////////
77dbe708 458
77dbe708 459
612607bd 460
b9f49659 461///////////////////////////////////////////////////////////////////
462///// Internal API: used by gradebook plugins and Moodle core /////
463///////////////////////////////////////////////////////////////////
e0724506 464
465/**
466 * Returns course gradebook setting
467 * @param int $courseid
468 * @param string $name of setting, maybe null if reset only
469 * @param bool $resetcache force reset of internal static cache
470 * @return string value, NULL if no setting
471 */
472function grade_get_setting($courseid, $name, $default=null, $resetcache=false) {
473 static $cache = array();
474
475 if ($resetcache or !array_key_exists($courseid, $cache)) {
476 $cache[$courseid] = array();
477
478 } else if (is_null($name)) {
479 return null;
480
481 } else if (array_key_exists($name, $cache[$courseid])) {
482 return $cache[$courseid][$name];
483 }
484
485 if (!$data = get_record('grade_settings', 'courseid', $courseid, 'name', addslashes($name))) {
486 $result = null;
487 } else {
488 $result = $data->value;
489 }
490
491 if (is_null($result)) {
492 $result = $default;
493 }
494
495 $cache[$courseid][$name] = $result;
496 return $result;
497}
498
26ed0305 499/**
500 * Returns all course gradebook settings as object properties
501 * @param int $courseid
502 * @return object
503 */
504function grade_get_settings($courseid) {
505 $settings = new object();
506 $settings->id = $courseid;
507
508 if ($records = get_records('grade_settings', 'courseid', $courseid)) {
509 foreach ($records as $record) {
510 $settings->{$record->name} = $record->value;
511 }
512 }
513
514 return $settings;
515}
516
e0724506 517/**
518 * Add/update course gradebook setting
519 * @param int $courseid
520 * @param string $name of setting
521 * @param string value, NULL means no setting==remove
522 * @return void
523 */
524function grade_set_setting($courseid, $name, $value) {
525 if (is_null($value)) {
526 delete_records('grade_settings', 'courseid', $courseid, 'name', addslashes($name));
527
528 } else if (!$existing = get_record('grade_settings', 'courseid', $courseid, 'name', addslashes($name))) {
529 $data = new object();
530 $data->courseid = $courseid;
531 $data->name = addslashes($name);
532 $data->value = addslashes($value);
533 insert_record('grade_settings', $data);
534
535 } else {
536 $data = new object();
537 $data->id = $existing->id;
538 $data->value = addslashes($value);
539 update_record('grade_settings', $data);
540 }
541
542 grade_get_setting($courseid, null, null, true); // reset the cache
543}
544
e9096dc2 545/**
546 * Returns string representation of grade value
547 * @param float $value grade value
548 * @param object $grade_item - by reference to prevent scale reloading
549 * @param bool $localized use localised decimal separator
b9f49659 550 * @param int $displaytype type of display - GRADE_DISPLAY_TYPE_REAL, GRADE_DISPLAY_TYPE_PERCENTAGE, GRADE_DISPLAY_TYPE_LETTER
e9096dc2 551 * @param int $decimalplaces number of decimal places when displaying float values
552 * @return string
553 */
554function grade_format_gradevalue($value, &$grade_item, $localized=true, $displaytype=null, $decimals=null) {
555 if ($grade_item->gradetype == GRADE_TYPE_NONE or $grade_item->gradetype == GRADE_TYPE_TEXT) {
556 return '';
557 }
558
559 // no grade yet?
560 if (is_null($value)) {
561 return '-';
562 }
563
564 if ($grade_item->gradetype != GRADE_TYPE_VALUE and $grade_item->gradetype != GRADE_TYPE_SCALE) {
565 //unknown type??
566 return '';
567 }
568
569 if (is_null($displaytype)) {
570 $displaytype = $grade_item->get_displaytype();
571 }
572
573 if (is_null($decimals)) {
574 $decimals = $grade_item->get_decimals();
575 }
576
577 switch ($displaytype) {
578 case GRADE_DISPLAY_TYPE_REAL:
579 if ($grade_item->gradetype == GRADE_TYPE_SCALE) {
1878f55d 580 if (!$scale = $grade_item->load_scale()) {
581 return get_string('error');
582 }
583
e9096dc2 584 $value = (int)bounded_number($grade_item->grademin, $value, $grade_item->grademax);
585 return format_string($scale->scale_items[$value-1]);
586
587 } else {
588 return format_float($value, $decimals, $localized);
589 }
590
591 case GRADE_DISPLAY_TYPE_PERCENTAGE:
592 $min = $grade_item->grademin;
593 $max = $grade_item->grademax;
594 if ($min == $max) {
595 return '';
596 }
597 $value = bounded_number($min, $value, $max);
598 $percentage = (($value-$min)*100)/($max-$min);
599 return format_float($percentage, $decimals, $localized).' %';
600
601 case GRADE_DISPLAY_TYPE_LETTER:
602 $context = get_context_instance(CONTEXT_COURSE, $grade_item->courseid);
603 if (!$letters = grade_get_letters($context)) {
604 return ''; // no letters??
605 }
606
607 $value = grade_grade::standardise_score($value, $grade_item->grademin, $grade_item->grademax, 0, 100);
608 $value = bounded_number(0, $value, 100); // just in case
609 foreach ($letters as $boundary => $letter) {
610 if ($value >= $boundary) {
611 return format_string($letter);
612 }
613 }
614 return '-'; // no match? maybe '' would be more correct
615
616 default:
617 return '';
618 }
619}
4b86bb08 620
621/**
622 * Returns grade options for gradebook category menu
623 * @param int $courseid
624 * @param bool $includenew include option for new category (-1)
625 * @return array of grade categories in course
626 */
627function grade_get_categories_menu($courseid, $includenew=false) {
628 $result = array();
629 if (!$categories = grade_category::fetch_all(array('courseid'=>$courseid))) {
630 //make sure course category exists
29c660c4 631 if (!grade_category::fetch_course_category($courseid)) {
4b86bb08 632 debugging('Can not create course grade category!');
633 return $result;
634 }
635 $categories = grade_category::fetch_all(array('courseid'=>$courseid));
636 }
637 foreach ($categories as $key=>$category) {
638 if ($category->is_course_category()) {
639 $result[$category->id] = get_string('uncategorised', 'grades');
640 unset($categories[$key]);
641 }
642 }
643 if ($includenew) {
644 $result[-1] = get_string('newcategory', 'grades');
645 }
646 $cats = array();
647 foreach ($categories as $category) {
648 $cats[$category->id] = $category->get_name();
649 }
650 asort($cats, SORT_LOCALE_STRING);
651
652 return ($result+$cats);
653}
e9096dc2 654
655/**
656 * Returns grade letters array used in context
657 * @param object $context object or null for defaults
658 * @return array of grade_boundary=>letter_string
659 */
660function grade_get_letters($context=null) {
661 if (empty($context)) {
284abb09 662 //default grading letters
663 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 664 }
665
666 static $cache = array();
667
668 if (array_key_exists($context->id, $cache)) {
669 return $cache[$context->id];
670 }
671
672 if (count($cache) > 100) {
673 $cache = array(); // cache size limit
674 }
675
676 $letters = array();
677
678 $contexts = get_parent_contexts($context);
679 array_unshift($contexts, $context->id);
680
681 foreach ($contexts as $ctxid) {
284abb09 682 if ($records = get_records('grade_letters', 'contextid', $ctxid, 'lowerboundary DESC')) {
e9096dc2 683 foreach ($records as $record) {
284abb09 684 $letters[$record->lowerboundary] = $record->letter;
e9096dc2 685 }
686 }
687
688 if (!empty($letters)) {
689 $cache[$context->id] = $letters;
690 return $letters;
691 }
692 }
693
694 $letters = grade_get_letters(null);
695 $cache[$context->id] = $letters;
696 return $letters;
697}
698
60243313 699
700/**
2c5e52e2 701 * Verify new value of idnumber - checks for uniqueness of new idnumbers, old are kept intact
60243313 702 * @param string idnumber string (with magic quotes)
204175c5 703 * @param int $courseid - id numbers are course unique only
60243313 704 * @param object $cm used for course module idnumbers and items attached to modules
705 * @param object $gradeitem is item idnumber
706 * @return boolean true means idnumber ok
707 */
204175c5 708function grade_verify_idnumber($idnumber, $courseid, $grade_item=null, $cm=null) {
60243313 709 if ($idnumber == '') {
710 //we allow empty idnumbers
711 return true;
712 }
713
714 // keep existing even when not unique
715 if ($cm and $cm->idnumber == $idnumber) {
716 return true;
717 } else if ($grade_item and $grade_item->idnumber == $idnumber) {
718 return true;
719 }
720
204175c5 721 if (get_records_select('course_modules', "course = $courseid AND idnumber='$idnumber'")) {
60243313 722 return false;
723 }
724
204175c5 725 if (get_records_select('grade_items', "courseid = $courseid AND idnumber='$idnumber'")) {
60243313 726 return false;
727 }
728
729 return true;
730}
731
732/**
733 * Force final grade recalculation in all course items
734 * @param int $courseid
735 */
f8e6e4db 736function grade_force_full_regrading($courseid) {
737 set_field('grade_items', 'needsupdate', 1, 'courseid', $courseid);
738}
34e67f76 739
5834dcdb 740/**
ac9b0805 741 * Updates all final grades in course.
a8995b34 742 *
743 * @param int $courseid
f8e6e4db 744 * @param int $userid if specified, try to do a quick regrading of grades of this user only
745 * @param object $updated_item the item in which
746 * @return boolean true if ok, array of errors if problems found (item id is used as key)
a8995b34 747 */
c86caae7 748function grade_regrade_final_grades($courseid, $userid=null, $updated_item=null) {
b8ff92b6 749
514a3467 750 $course_item = grade_item::fetch_course_item($courseid);
f04873a9 751
f8e6e4db 752 if ($userid) {
753 // one raw grade updated for one user
754 if (empty($updated_item)) {
755 error("updated_item_id can not be null!");
756 }
757 if ($course_item->needsupdate) {
758 $updated_item->force_regrading();
759 return 'Can not do fast regrading after updating of raw grades';
a8995b34 760 }
772ddfbf 761
f8e6e4db 762 } else {
763 if (!$course_item->needsupdate) {
764 // nothing to do :-)
b8ff92b6 765 return true;
b8ff92b6 766 }
a8995b34 767 }
768
f8e6e4db 769 $grade_items = grade_item::fetch_all(array('courseid'=>$courseid));
770 $depends_on = array();
771
772 // first mark all category and calculated items as needing regrading
fb0e3570 773 // this is slower, but 100% accurate
f8e6e4db 774 foreach ($grade_items as $gid=>$gitem) {
fb46b5b6 775 if (!empty($updated_item) and $updated_item->id == $gid) {
f8e6e4db 776 $grade_items[$gid]->needsupdate = 1;
777
eacd3700 778 } else if ($gitem->is_course_item() or $gitem->is_category_item() or $gitem->is_calculated()) {
f8e6e4db 779 $grade_items[$gid]->needsupdate = 1;
780 }
2e53372c 781
f8e6e4db 782 // construct depends_on lookup array
783 $depends_on[$gid] = $grade_items[$gid]->depends_on();
784 }
2e53372c 785
d14ae855 786 $errors = array();
b8ff92b6 787 $finalids = array();
d14ae855 788 $gids = array_keys($grade_items);
eacd3700 789 $failed = 0;
d14ae855 790
791 while (count($finalids) < count($gids)) { // work until all grades are final or error found
792 $count = 0;
793 foreach ($gids as $gid) {
794 if (in_array($gid, $finalids)) {
795 continue; // already final
796 }
797
798 if (!$grade_items[$gid]->needsupdate) {
799 $finalids[] = $gid; // we can make it final - does not need update
b8ff92b6 800 continue;
801 }
802
b8ff92b6 803 $doupdate = true;
f8e6e4db 804 foreach ($depends_on[$gid] as $did) {
b8ff92b6 805 if (!in_array($did, $finalids)) {
806 $doupdate = false;
d14ae855 807 continue; // this item depends on something that is not yet in finals array
b8ff92b6 808 }
809 }
810
811 //oki - let's update, calculate or aggregate :-)
812 if ($doupdate) {
d14ae855 813 $result = $grade_items[$gid]->regrade_final_grades($userid);
f8e6e4db 814
815 if ($result === true) {
d14ae855 816 $grade_items[$gid]->regrading_finished();
fb0e3570 817 $grade_items[$gid]->check_locktime(); // do the locktime item locking
f8e6e4db 818 $count++;
b8ff92b6 819 $finalids[] = $gid;
fb0e3570 820
f8e6e4db 821 } else {
d14ae855 822 $grade_items[$gid]->force_regrading();
f8e6e4db 823 $errors[$gid] = $result;
b8ff92b6 824 }
825 }
826 }
827
828 if ($count == 0) {
eacd3700 829 $failed++;
830 } else {
831 $failed = 0;
832 }
833
834 if ($failed > 1) {
d14ae855 835 foreach($gids as $gid) {
836 if (in_array($gid, $finalids)) {
837 continue; // this one is ok
838 }
839 $grade_items[$gid]->force_regrading();
840 $errors[$grade_items[$gid]->id] = 'Probably circular reference or broken calculation formula'; // TODO: localize
b8ff92b6 841 }
d14ae855 842 break; // oki, found error
b8ff92b6 843 }
844 }
845
846 if (count($errors) == 0) {
fb0e3570 847 if (empty($userid)) {
848 // do the locktime locking of grades, but only when doing full regrading
fed7cdc9 849 grade_grade::check_locktime_all($gids);
fb0e3570 850 }
b8ff92b6 851 return true;
852 } else {
853 return $errors;
854 }
a8995b34 855}
967f222f 856
de420c11 857/**
d185c3ee 858 * For backwards compatibility with old third-party modules, this function can
859 * be used to import all grades from activities with legacy grading.
f0362b5d 860 * @param int $courseid
967f222f 861 */
f0362b5d 862function grade_grab_legacy_grades($courseid) {
ac9b0805 863 global $CFG;
967f222f 864
865 if (!$mods = get_list_of_plugins('mod') ) {
866 error('No modules installed!');
867 }
868
869 foreach ($mods as $mod) {
967f222f 870 if ($mod == 'NEWMODULE') { // Someone has unzipped the template, ignore it
871 continue;
872 }
873
d185c3ee 874 $fullmod = $CFG->dirroot.'/mod/'.$mod;
967f222f 875
876 // include the module lib once
877 if (file_exists($fullmod.'/lib.php')) {
878 include_once($fullmod.'/lib.php');
de420c11 879 // look for modname_grades() function - old gradebook pulling function
880 // if present sync the grades with new grading system
967f222f 881 $gradefunc = $mod.'_grades';
de420c11 882 if (function_exists($gradefunc)) {
f0362b5d 883 grade_grab_course_grades($courseid, $mod);
967f222f 884 }
885 }
886 }
887}
888
ac9b0805 889/**
f0362b5d 890 * Refetches data from all course activities
891 * @param int $courseid
892 * @param string $modname
893 * @return success
ac9b0805 894 */
f0362b5d 895function grade_grab_course_grades($courseid, $modname=null) {
ac9b0805 896 global $CFG;
897
f0362b5d 898 if ($modname) {
899 $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname
900 FROM {$CFG->prefix}$modname a, {$CFG->prefix}course_modules cm, {$CFG->prefix}modules m
901 WHERE m.name='$modname' AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=$courseid";
902
903 if ($modinstances = get_records_sql($sql)) {
904 foreach ($modinstances as $modinstance) {
905 grade_update_mod_grades($modinstance);
906 }
907 }
908 return;
909 }
910
ac9b0805 911 if (!$mods = get_list_of_plugins('mod') ) {
912 error('No modules installed!');
913 }
914
915 foreach ($mods as $mod) {
ac9b0805 916 if ($mod == 'NEWMODULE') { // Someone has unzipped the template, ignore it
917 continue;
918 }
919
ac9b0805 920 $fullmod = $CFG->dirroot.'/mod/'.$mod;
921
922 // include the module lib once
923 if (file_exists($fullmod.'/lib.php')) {
f0362b5d 924 // get all instance of the activity
925 $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname
926 FROM {$CFG->prefix}$mod a, {$CFG->prefix}course_modules cm, {$CFG->prefix}modules m
927 WHERE m.name='$mod' AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=$courseid";
928
929 if ($modinstances = get_records_sql($sql)) {
930 foreach ($modinstances as $modinstance) {
931 grade_update_mod_grades($modinstance);
932 }
ac9b0805 933 }
934 }
935 }
936}
937
d185c3ee 938/**
939 * Force full update of module grades in central gradebook - works for both legacy and converted activities.
940 * @param object $modinstance object with extra cmidnumber and modname property
941 * @return boolean success
942 */
2b0f65e2 943function grade_update_mod_grades($modinstance, $userid=0) {
d185c3ee 944 global $CFG;
945
946 $fullmod = $CFG->dirroot.'/mod/'.$modinstance->modname;
947 if (!file_exists($fullmod.'/lib.php')) {
948 debugging('missing lib.php file in module');
949 return false;
950 }
951 include_once($fullmod.'/lib.php');
952
953 // does it use legacy grading?
954 $gradefunc = $modinstance->modname.'_grades';
955 $updategradesfunc = $modinstance->modname.'_update_grades';
956 $updateitemfunc = $modinstance->modname.'_grade_item_update';
957
958 if (function_exists($gradefunc)) {
2b0f65e2 959
960 // legacy module - not yet converted
d185c3ee 961 if ($oldgrades = $gradefunc($modinstance->id)) {
962
963 $grademax = $oldgrades->maxgrade;
964 $scaleid = NULL;
965 if (!is_numeric($grademax)) {
966 // scale name is provided as a string, try to find it
967 if (!$scale = get_record('scale', 'name', $grademax)) {
968 debugging('Incorrect scale name! name:'.$grademax);
969 return false;
970 }
971 $scaleid = $scale->id;
972 }
973
974 if (!$grade_item = grade_get_legacy_grade_item($modinstance, $grademax, $scaleid)) {
975 debugging('Can not get/create legacy grade item!');
976 return false;
977 }
978
0b5a80a1 979 if (!empty($oldgrades->grades)) {
980 $grades = array();
d185c3ee 981
0b5a80a1 982 foreach ($oldgrades->grades as $uid=>$usergrade) {
983 if ($userid and $uid != $userid) {
984 continue;
985 }
986 $grade = new object();
987 $grade->userid = $uid;
d185c3ee 988
0b5a80a1 989 if ($usergrade == '-') {
990 // no grade
991 $grade->rawgrade = null;
d185c3ee 992
0b5a80a1 993 } else if ($scaleid) {
994 // scale in use, words used
995 $gradescale = explode(",", $scale->scale);
996 $grade->rawgrade = array_search($usergrade, $gradescale) + 1;
997
998 } else {
999 // good old numeric value
1000 $grade->rawgrade = $usergrade;
1001 }
1002 $grades[] = $grade;
d185c3ee 1003 }
d185c3ee 1004
0b5a80a1 1005 grade_update('legacygrab', $grade_item->courseid, $grade_item->itemtype, $grade_item->itemmodule,
1006 $grade_item->iteminstance, $grade_item->itemnumber, $grades);
1007 }
d185c3ee 1008 }
1009
1010 } else if (function_exists($updategradesfunc) and function_exists($updateitemfunc)) {
1011 //new grading supported, force updating of grades
1012 $updateitemfunc($modinstance);
2b0f65e2 1013 $updategradesfunc($modinstance, $userid);
d185c3ee 1014
1015 } else {
2b0f65e2 1016 // mudule does not support grading??
d185c3ee 1017 }
1018
1019 return true;
1020}
de420c11 1021
1022/**
d185c3ee 1023 * Get and update/create grade item for legacy modules.
de420c11 1024 */
1025function grade_get_legacy_grade_item($modinstance, $grademax, $scaleid) {
1026
1027 // does it already exist?
42ff9ce6 1028 if ($grade_items = grade_item::fetch_all(array('courseid'=>$modinstance->course, 'itemtype'=>'mod', 'itemmodule'=>$modinstance->modname, 'iteminstance'=>$modinstance->id, 'itemnumber'=>0))) {
de420c11 1029 if (count($grade_items) > 1) {
d185c3ee 1030 debugging('Multiple legacy grade_items found.');
de420c11 1031 return false;
1032 }
1033
1034 $grade_item = reset($grade_items);
de420c11 1035
d185c3ee 1036 if (is_null($grademax) and is_null($scaleid)) {
1037 $grade_item->gradetype = GRADE_TYPE_NONE;
de420c11 1038
d185c3ee 1039 } else if ($scaleid) {
1040 $grade_item->gradetype = GRADE_TYPE_SCALE;
1041 $grade_item->scaleid = $scaleid;
97d608ba 1042 $grade_item->grademin = 1;
de420c11 1043
d185c3ee 1044 } else {
97d608ba 1045 $grade_item->gradetype = GRADE_TYPE_VALUE;
1046 $grade_item->grademax = $grademax;
1047 $grade_item->grademin = 0;
de420c11 1048 }
1049
d185c3ee 1050 $grade_item->itemname = $modinstance->name;
1051 $grade_item->idnumber = $modinstance->cmidnumber;
de420c11 1052
d185c3ee 1053 $grade_item->update();
de420c11 1054
1055 return $grade_item;
1056 }
612607bd 1057
de420c11 1058 // create new one
d185c3ee 1059 $params = array('courseid' =>$modinstance->course,
de420c11 1060 'itemtype' =>'mod',
1061 'itemmodule' =>$modinstance->modname,
1062 'iteminstance'=>$modinstance->id,
d185c3ee 1063 'itemnumber' =>0,
de420c11 1064 'itemname' =>$modinstance->name,
1065 'idnumber' =>$modinstance->cmidnumber);
1066
d185c3ee 1067 if (is_null($grademax) and is_null($scaleid)) {
1068 $params['gradetype'] = GRADE_TYPE_NONE;
1069
1070 } else if ($scaleid) {
612607bd 1071 $params['gradetype'] = GRADE_TYPE_SCALE;
de420c11 1072 $params['scaleid'] = $scaleid;
b3ac6c3e 1073 $grade_item->grademin = 1;
de420c11 1074 } else {
612607bd 1075 $params['gradetype'] = GRADE_TYPE_VALUE;
de420c11 1076 $params['grademax'] = $grademax;
1077 $params['grademin'] = 0;
1078 }
1079
f70152b7 1080 $grade_item = new grade_item($params);
1081 $grade_item->insert();
de420c11 1082
f70152b7 1083 return $grade_item;
de420c11 1084}
1085
b51ece5b 1086/**
1087 * Remove grade letters for given context
1088 * @param object $context
1089 */
1090function remove_grade_letters($context, $showfeedback) {
1091 $strdeleted = get_string('deleted');
1092
1093 delete_records('grade_letters', 'contextid', $context->id);
1094 if ($showfeedback) {
1095 notify($strdeleted.' - '.get_string('letters', 'grades'));
1096 }
1097}
f615fbab 1098/**
1099 * Remove all grade related course data - history is kept
1100 * @param int $courseid
6b5c722d 1101 * @param bool @showfeedback print feedback
f615fbab 1102 */
1103function remove_course_grades($courseid, $showfeedback) {
1104 $strdeleted = get_string('deleted');
1105
1106 $course_category = grade_category::fetch_course_category($courseid);
1107 $course_category->delete('coursedelete');
1108 if ($showfeedback) {
1109 notify($strdeleted.' - '.get_string('grades', 'grades').', '.get_string('items', 'grades').', '.get_string('categories', 'grades'));
1110 }
1111
1112 if ($outcomes = grade_outcome::fetch_all(array('courseid'=>$courseid))) {
1113 foreach ($outcomes as $outcome) {
1114 $outcome->delete('coursedelete');
1115 }
1116 }
1117 delete_records('grade_outcomes_courses', 'courseid', $courseid);
1118 if ($showfeedback) {
1119 notify($strdeleted.' - '.get_string('outcomes', 'grades'));
1120 }
1121
1122 if ($scales = grade_scale::fetch_all(array('courseid'=>$courseid))) {
1123 foreach ($scales as $scale) {
1124 $scale->delete('coursedelete');
1125 }
1126 }
1127 if ($showfeedback) {
1128 notify($strdeleted.' - '.get_string('scales'));
1129 }
b51ece5b 1130
1131 delete_records('grade_settings', 'courseid', $courseid);
1132 if ($showfeedback) {
1133 notify($strdeleted.' - '.get_string('settings', 'grades'));
1134 }
f615fbab 1135}
bfe7297e 1136
8a0a6046 1137/**
1138 * Does gradebook cleanup when module uninstalled.
1139 */
1140function grade_uninstalled_module($modname) {
1141 global $CFG;
1142
1143 $sql = "SELECT *
1144 FROM {$CFG->prefix}grade_items
1145 WHERE itemtype='mod' AND itemmodule='$modname'";
1146
1147 // go all items for this module and delete them including the grades
1148 if ($rs = get_recordset_sql($sql)) {
1149 while ($item = rs_fetch_next_record($rs)) {
1150 $grade_item = new grade_item($item, false);
1151 $grade_item->delete('moduninstall');
1152 }
1153 rs_close($rs);
1154 }
1155}
1156
2650c51e 1157/**
1158 * Grading cron job
1159 */
1160function grade_cron() {
26101be8 1161 global $CFG;
1162
1163 $now = time();
1164
1165 $sql = "SELECT i.*
1166 FROM {$CFG->prefix}grade_items i
1167 WHERE i.locked = 0 AND i.locktime > 0 AND i.locktime < $now AND EXISTS (
1f4a0320 1168 SELECT 'x' FROM {$CFG->prefix}grade_items c WHERE c.itemtype='course' AND c.needsupdate=0 AND c.courseid=i.courseid)";
26101be8 1169
2650c51e 1170 // go through all courses that have proper final grades and lock them if needed
26101be8 1171 if ($rs = get_recordset_sql($sql)) {
03cedd62 1172 while ($item = rs_fetch_next_record($rs)) {
1173 $grade_item = new grade_item($item, false);
1174 $grade_item->locked = $now;
1175 $grade_item->update('locktime');
2650c51e 1176 }
1177 rs_close($rs);
1178 }
26101be8 1179
fcac8e51 1180 $grade_inst = new grade_grade();
1181 $fields = 'g.'.implode(',g.', $grade_inst->required_fields);
1182
1183 $sql = "SELECT $fields
26101be8 1184 FROM {$CFG->prefix}grade_grades g, {$CFG->prefix}grade_items i
1185 WHERE g.locked = 0 AND g.locktime > 0 AND g.locktime < $now AND g.itemid=i.id AND EXISTS (
1f4a0320 1186 SELECT 'x' FROM {$CFG->prefix}grade_items c WHERE c.itemtype='course' AND c.needsupdate=0 AND c.courseid=i.courseid)";
26101be8 1187
1188 // go through all courses that have proper final grades and lock them if needed
1189 if ($rs = get_recordset_sql($sql)) {
03cedd62 1190 while ($grade = rs_fetch_next_record($rs)) {
1191 $grade_grade = new grade_grade($grade, false);
1192 $grade_grade->locked = $now;
1193 $grade_grade->update('locktime');
26101be8 1194 }
1195 rs_close($rs);
1196 }
1197
1ee0df06 1198 //TODO: do not run this cleanup every cron invocation
1199 // cleanup history tables
f0362b5d 1200 if (!empty($CFG->gradehistorylifetime)) { // value in days
1201 $histlifetime = $now - ($CFG->gradehistorylifetime * 3600 * 24);
1202 $tables = array('grade_outcomes_history', 'grade_categories_history', 'grade_items_history', 'grade_grades_history', 'scale_history');
1203 foreach ($tables as $table) {
1204 if (delete_records_select($table, "timemodified < $histlifetime")) {
1205 mtrace(" Deleted old grade history records from '$table'");
1ee0df06 1206 }
1207 }
f0362b5d 1208 }
1209}
1210
1211/**
1212 * Resel all course grades
1213 * @param int $courseid
1214 * @return success
1215 */
1216function grade_course_reset($courseid) {
1217
1218 // no recalculations
1219 grade_force_full_regrading($courseid);
1220
1221 $grade_items = grade_item::fetch_all(array('courseid'=>$courseid));
1222 foreach ($grade_items as $gid=>$grade_item) {
1223 $grade_item->delete_all_grades('reset');
1224 }
1ee0df06 1225
f0362b5d 1226 //refetch all grades
1227 grade_grab_course_grades($courseid);
1ee0df06 1228
f0362b5d 1229 // recalculate all grades
1230 grade_regrade_final_grades($courseid);
1231 return true;
2650c51e 1232}
1233
60cf7430 1234?>