Automatic installer.php lang files by installer_builder (20080308)
[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 }
d19f4770 442
443 if (isset($return->outcomes[$grade_item->itemnumber])) {
444 // itemnumber duplicates - lets fix them!
445 $newnumber = $grade_item->itemnumber + 1;
446 while(grade_item::fetch(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid, 'itemnumber'=>$newnumber))) {
447 $newnumber++;
448 }
449 $outcome->itemnumber = $newnumber;
450 $grade_item->itemnumber = $newnumber;
451 $grade_item->update('system');
452 }
453
d886a7ea 454 $return->outcomes[$grade_item->itemnumber] = $outcome;
fcac8e51 455
456 }
6b5c722d 457 }
458 }
459
fcac8e51 460 // sort results using itemnumbers
461 ksort($return->items, SORT_NUMERIC);
462 ksort($return->outcomes, SORT_NUMERIC);
463
464 return $return;
6b5c722d 465}
466
b9f49659 467///////////////////////////////////////////////////////////////////
468///// End of public API for communication with modules/blocks /////
469///////////////////////////////////////////////////////////////////
77dbe708 470
77dbe708 471
612607bd 472
b9f49659 473///////////////////////////////////////////////////////////////////
474///// Internal API: used by gradebook plugins and Moodle core /////
475///////////////////////////////////////////////////////////////////
e0724506 476
477/**
478 * Returns course gradebook setting
479 * @param int $courseid
480 * @param string $name of setting, maybe null if reset only
481 * @param bool $resetcache force reset of internal static cache
482 * @return string value, NULL if no setting
483 */
484function grade_get_setting($courseid, $name, $default=null, $resetcache=false) {
485 static $cache = array();
486
487 if ($resetcache or !array_key_exists($courseid, $cache)) {
488 $cache[$courseid] = array();
489
490 } else if (is_null($name)) {
491 return null;
492
493 } else if (array_key_exists($name, $cache[$courseid])) {
494 return $cache[$courseid][$name];
495 }
496
497 if (!$data = get_record('grade_settings', 'courseid', $courseid, 'name', addslashes($name))) {
498 $result = null;
499 } else {
500 $result = $data->value;
501 }
502
503 if (is_null($result)) {
504 $result = $default;
505 }
506
507 $cache[$courseid][$name] = $result;
508 return $result;
509}
510
26ed0305 511/**
512 * Returns all course gradebook settings as object properties
513 * @param int $courseid
514 * @return object
515 */
516function grade_get_settings($courseid) {
517 $settings = new object();
518 $settings->id = $courseid;
519
520 if ($records = get_records('grade_settings', 'courseid', $courseid)) {
521 foreach ($records as $record) {
522 $settings->{$record->name} = $record->value;
523 }
524 }
525
526 return $settings;
527}
528
e0724506 529/**
530 * Add/update course gradebook setting
531 * @param int $courseid
532 * @param string $name of setting
533 * @param string value, NULL means no setting==remove
534 * @return void
535 */
536function grade_set_setting($courseid, $name, $value) {
537 if (is_null($value)) {
538 delete_records('grade_settings', 'courseid', $courseid, 'name', addslashes($name));
539
540 } else if (!$existing = get_record('grade_settings', 'courseid', $courseid, 'name', addslashes($name))) {
541 $data = new object();
542 $data->courseid = $courseid;
543 $data->name = addslashes($name);
544 $data->value = addslashes($value);
545 insert_record('grade_settings', $data);
546
547 } else {
548 $data = new object();
549 $data->id = $existing->id;
550 $data->value = addslashes($value);
551 update_record('grade_settings', $data);
552 }
553
554 grade_get_setting($courseid, null, null, true); // reset the cache
555}
556
e9096dc2 557/**
558 * Returns string representation of grade value
559 * @param float $value grade value
560 * @param object $grade_item - by reference to prevent scale reloading
561 * @param bool $localized use localised decimal separator
b9f49659 562 * @param int $displaytype type of display - GRADE_DISPLAY_TYPE_REAL, GRADE_DISPLAY_TYPE_PERCENTAGE, GRADE_DISPLAY_TYPE_LETTER
e9096dc2 563 * @param int $decimalplaces number of decimal places when displaying float values
564 * @return string
565 */
566function grade_format_gradevalue($value, &$grade_item, $localized=true, $displaytype=null, $decimals=null) {
567 if ($grade_item->gradetype == GRADE_TYPE_NONE or $grade_item->gradetype == GRADE_TYPE_TEXT) {
568 return '';
569 }
570
571 // no grade yet?
572 if (is_null($value)) {
573 return '-';
574 }
575
576 if ($grade_item->gradetype != GRADE_TYPE_VALUE and $grade_item->gradetype != GRADE_TYPE_SCALE) {
577 //unknown type??
578 return '';
579 }
580
581 if (is_null($displaytype)) {
582 $displaytype = $grade_item->get_displaytype();
583 }
584
585 if (is_null($decimals)) {
586 $decimals = $grade_item->get_decimals();
587 }
588
589 switch ($displaytype) {
590 case GRADE_DISPLAY_TYPE_REAL:
591 if ($grade_item->gradetype == GRADE_TYPE_SCALE) {
1878f55d 592 if (!$scale = $grade_item->load_scale()) {
593 return get_string('error');
594 }
595
e9096dc2 596 $value = (int)bounded_number($grade_item->grademin, $value, $grade_item->grademax);
597 return format_string($scale->scale_items[$value-1]);
598
599 } else {
600 return format_float($value, $decimals, $localized);
601 }
602
603 case GRADE_DISPLAY_TYPE_PERCENTAGE:
604 $min = $grade_item->grademin;
605 $max = $grade_item->grademax;
606 if ($min == $max) {
607 return '';
608 }
609 $value = bounded_number($min, $value, $max);
610 $percentage = (($value-$min)*100)/($max-$min);
611 return format_float($percentage, $decimals, $localized).' %';
612
613 case GRADE_DISPLAY_TYPE_LETTER:
614 $context = get_context_instance(CONTEXT_COURSE, $grade_item->courseid);
615 if (!$letters = grade_get_letters($context)) {
616 return ''; // no letters??
617 }
618
619 $value = grade_grade::standardise_score($value, $grade_item->grademin, $grade_item->grademax, 0, 100);
620 $value = bounded_number(0, $value, 100); // just in case
621 foreach ($letters as $boundary => $letter) {
622 if ($value >= $boundary) {
623 return format_string($letter);
624 }
625 }
626 return '-'; // no match? maybe '' would be more correct
627
628 default:
629 return '';
630 }
631}
4b86bb08 632
633/**
634 * Returns grade options for gradebook category menu
635 * @param int $courseid
636 * @param bool $includenew include option for new category (-1)
637 * @return array of grade categories in course
638 */
639function grade_get_categories_menu($courseid, $includenew=false) {
640 $result = array();
641 if (!$categories = grade_category::fetch_all(array('courseid'=>$courseid))) {
642 //make sure course category exists
29c660c4 643 if (!grade_category::fetch_course_category($courseid)) {
4b86bb08 644 debugging('Can not create course grade category!');
645 return $result;
646 }
647 $categories = grade_category::fetch_all(array('courseid'=>$courseid));
648 }
649 foreach ($categories as $key=>$category) {
650 if ($category->is_course_category()) {
651 $result[$category->id] = get_string('uncategorised', 'grades');
652 unset($categories[$key]);
653 }
654 }
655 if ($includenew) {
656 $result[-1] = get_string('newcategory', 'grades');
657 }
658 $cats = array();
659 foreach ($categories as $category) {
660 $cats[$category->id] = $category->get_name();
661 }
662 asort($cats, SORT_LOCALE_STRING);
663
664 return ($result+$cats);
665}
e9096dc2 666
667/**
668 * Returns grade letters array used in context
669 * @param object $context object or null for defaults
670 * @return array of grade_boundary=>letter_string
671 */
672function grade_get_letters($context=null) {
673 if (empty($context)) {
284abb09 674 //default grading letters
675 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 676 }
677
678 static $cache = array();
679
680 if (array_key_exists($context->id, $cache)) {
681 return $cache[$context->id];
682 }
683
684 if (count($cache) > 100) {
685 $cache = array(); // cache size limit
686 }
687
688 $letters = array();
689
690 $contexts = get_parent_contexts($context);
691 array_unshift($contexts, $context->id);
692
693 foreach ($contexts as $ctxid) {
284abb09 694 if ($records = get_records('grade_letters', 'contextid', $ctxid, 'lowerboundary DESC')) {
e9096dc2 695 foreach ($records as $record) {
284abb09 696 $letters[$record->lowerboundary] = $record->letter;
e9096dc2 697 }
698 }
699
700 if (!empty($letters)) {
701 $cache[$context->id] = $letters;
702 return $letters;
703 }
704 }
705
706 $letters = grade_get_letters(null);
707 $cache[$context->id] = $letters;
708 return $letters;
709}
710
60243313 711
712/**
2c5e52e2 713 * Verify new value of idnumber - checks for uniqueness of new idnumbers, old are kept intact
60243313 714 * @param string idnumber string (with magic quotes)
204175c5 715 * @param int $courseid - id numbers are course unique only
60243313 716 * @param object $cm used for course module idnumbers and items attached to modules
717 * @param object $gradeitem is item idnumber
718 * @return boolean true means idnumber ok
719 */
204175c5 720function grade_verify_idnumber($idnumber, $courseid, $grade_item=null, $cm=null) {
60243313 721 if ($idnumber == '') {
722 //we allow empty idnumbers
723 return true;
724 }
725
726 // keep existing even when not unique
727 if ($cm and $cm->idnumber == $idnumber) {
728 return true;
729 } else if ($grade_item and $grade_item->idnumber == $idnumber) {
730 return true;
731 }
732
204175c5 733 if (get_records_select('course_modules', "course = $courseid AND idnumber='$idnumber'")) {
60243313 734 return false;
735 }
736
204175c5 737 if (get_records_select('grade_items', "courseid = $courseid AND idnumber='$idnumber'")) {
60243313 738 return false;
739 }
740
741 return true;
742}
743
744/**
745 * Force final grade recalculation in all course items
746 * @param int $courseid
747 */
f8e6e4db 748function grade_force_full_regrading($courseid) {
749 set_field('grade_items', 'needsupdate', 1, 'courseid', $courseid);
750}
34e67f76 751
5834dcdb 752/**
ac9b0805 753 * Updates all final grades in course.
a8995b34 754 *
755 * @param int $courseid
f8e6e4db 756 * @param int $userid if specified, try to do a quick regrading of grades of this user only
757 * @param object $updated_item the item in which
758 * @return boolean true if ok, array of errors if problems found (item id is used as key)
a8995b34 759 */
c86caae7 760function grade_regrade_final_grades($courseid, $userid=null, $updated_item=null) {
b8ff92b6 761
514a3467 762 $course_item = grade_item::fetch_course_item($courseid);
f04873a9 763
f8e6e4db 764 if ($userid) {
765 // one raw grade updated for one user
766 if (empty($updated_item)) {
767 error("updated_item_id can not be null!");
768 }
769 if ($course_item->needsupdate) {
770 $updated_item->force_regrading();
b45d8391 771 return array($course_item->id =>'Can not do fast regrading after updating of raw grades');
a8995b34 772 }
772ddfbf 773
f8e6e4db 774 } else {
775 if (!$course_item->needsupdate) {
776 // nothing to do :-)
b8ff92b6 777 return true;
b8ff92b6 778 }
a8995b34 779 }
780
f8e6e4db 781 $grade_items = grade_item::fetch_all(array('courseid'=>$courseid));
782 $depends_on = array();
783
784 // first mark all category and calculated items as needing regrading
fb0e3570 785 // this is slower, but 100% accurate
f8e6e4db 786 foreach ($grade_items as $gid=>$gitem) {
fb46b5b6 787 if (!empty($updated_item) and $updated_item->id == $gid) {
f8e6e4db 788 $grade_items[$gid]->needsupdate = 1;
789
eacd3700 790 } else if ($gitem->is_course_item() or $gitem->is_category_item() or $gitem->is_calculated()) {
f8e6e4db 791 $grade_items[$gid]->needsupdate = 1;
792 }
2e53372c 793
f8e6e4db 794 // construct depends_on lookup array
795 $depends_on[$gid] = $grade_items[$gid]->depends_on();
796 }
2e53372c 797
d14ae855 798 $errors = array();
b8ff92b6 799 $finalids = array();
d14ae855 800 $gids = array_keys($grade_items);
eacd3700 801 $failed = 0;
d14ae855 802
803 while (count($finalids) < count($gids)) { // work until all grades are final or error found
804 $count = 0;
805 foreach ($gids as $gid) {
806 if (in_array($gid, $finalids)) {
807 continue; // already final
808 }
809
810 if (!$grade_items[$gid]->needsupdate) {
811 $finalids[] = $gid; // we can make it final - does not need update
b8ff92b6 812 continue;
813 }
814
b8ff92b6 815 $doupdate = true;
f8e6e4db 816 foreach ($depends_on[$gid] as $did) {
b8ff92b6 817 if (!in_array($did, $finalids)) {
818 $doupdate = false;
d14ae855 819 continue; // this item depends on something that is not yet in finals array
b8ff92b6 820 }
821 }
822
823 //oki - let's update, calculate or aggregate :-)
824 if ($doupdate) {
d14ae855 825 $result = $grade_items[$gid]->regrade_final_grades($userid);
f8e6e4db 826
827 if ($result === true) {
d14ae855 828 $grade_items[$gid]->regrading_finished();
fb0e3570 829 $grade_items[$gid]->check_locktime(); // do the locktime item locking
f8e6e4db 830 $count++;
b8ff92b6 831 $finalids[] = $gid;
fb0e3570 832
f8e6e4db 833 } else {
d14ae855 834 $grade_items[$gid]->force_regrading();
f8e6e4db 835 $errors[$gid] = $result;
b8ff92b6 836 }
837 }
838 }
839
840 if ($count == 0) {
eacd3700 841 $failed++;
842 } else {
843 $failed = 0;
844 }
845
846 if ($failed > 1) {
d14ae855 847 foreach($gids as $gid) {
848 if (in_array($gid, $finalids)) {
849 continue; // this one is ok
850 }
851 $grade_items[$gid]->force_regrading();
852 $errors[$grade_items[$gid]->id] = 'Probably circular reference or broken calculation formula'; // TODO: localize
b8ff92b6 853 }
d14ae855 854 break; // oki, found error
b8ff92b6 855 }
856 }
857
858 if (count($errors) == 0) {
fb0e3570 859 if (empty($userid)) {
860 // do the locktime locking of grades, but only when doing full regrading
fed7cdc9 861 grade_grade::check_locktime_all($gids);
fb0e3570 862 }
b8ff92b6 863 return true;
864 } else {
865 return $errors;
866 }
a8995b34 867}
967f222f 868
de420c11 869/**
d185c3ee 870 * For backwards compatibility with old third-party modules, this function can
871 * be used to import all grades from activities with legacy grading.
f0362b5d 872 * @param int $courseid
967f222f 873 */
f0362b5d 874function grade_grab_legacy_grades($courseid) {
ac9b0805 875 global $CFG;
967f222f 876
877 if (!$mods = get_list_of_plugins('mod') ) {
878 error('No modules installed!');
879 }
880
881 foreach ($mods as $mod) {
967f222f 882 if ($mod == 'NEWMODULE') { // Someone has unzipped the template, ignore it
883 continue;
884 }
885
d185c3ee 886 $fullmod = $CFG->dirroot.'/mod/'.$mod;
967f222f 887
888 // include the module lib once
889 if (file_exists($fullmod.'/lib.php')) {
890 include_once($fullmod.'/lib.php');
de420c11 891 // look for modname_grades() function - old gradebook pulling function
892 // if present sync the grades with new grading system
967f222f 893 $gradefunc = $mod.'_grades';
de420c11 894 if (function_exists($gradefunc)) {
f0362b5d 895 grade_grab_course_grades($courseid, $mod);
967f222f 896 }
897 }
898 }
899}
900
ac9b0805 901/**
f0362b5d 902 * Refetches data from all course activities
903 * @param int $courseid
904 * @param string $modname
905 * @return success
ac9b0805 906 */
f0362b5d 907function grade_grab_course_grades($courseid, $modname=null) {
ac9b0805 908 global $CFG;
909
f0362b5d 910 if ($modname) {
911 $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname
912 FROM {$CFG->prefix}$modname a, {$CFG->prefix}course_modules cm, {$CFG->prefix}modules m
913 WHERE m.name='$modname' AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=$courseid";
914
915 if ($modinstances = get_records_sql($sql)) {
916 foreach ($modinstances as $modinstance) {
917 grade_update_mod_grades($modinstance);
918 }
919 }
920 return;
921 }
922
ac9b0805 923 if (!$mods = get_list_of_plugins('mod') ) {
924 error('No modules installed!');
925 }
926
927 foreach ($mods as $mod) {
ac9b0805 928 if ($mod == 'NEWMODULE') { // Someone has unzipped the template, ignore it
929 continue;
930 }
931
ac9b0805 932 $fullmod = $CFG->dirroot.'/mod/'.$mod;
933
934 // include the module lib once
935 if (file_exists($fullmod.'/lib.php')) {
f0362b5d 936 // get all instance of the activity
937 $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname
938 FROM {$CFG->prefix}$mod a, {$CFG->prefix}course_modules cm, {$CFG->prefix}modules m
939 WHERE m.name='$mod' AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=$courseid";
940
941 if ($modinstances = get_records_sql($sql)) {
942 foreach ($modinstances as $modinstance) {
943 grade_update_mod_grades($modinstance);
944 }
ac9b0805 945 }
946 }
947 }
948}
949
d185c3ee 950/**
951 * Force full update of module grades in central gradebook - works for both legacy and converted activities.
952 * @param object $modinstance object with extra cmidnumber and modname property
953 * @return boolean success
954 */
2b0f65e2 955function grade_update_mod_grades($modinstance, $userid=0) {
d185c3ee 956 global $CFG;
957
958 $fullmod = $CFG->dirroot.'/mod/'.$modinstance->modname;
959 if (!file_exists($fullmod.'/lib.php')) {
960 debugging('missing lib.php file in module');
961 return false;
962 }
963 include_once($fullmod.'/lib.php');
964
965 // does it use legacy grading?
966 $gradefunc = $modinstance->modname.'_grades';
967 $updategradesfunc = $modinstance->modname.'_update_grades';
968 $updateitemfunc = $modinstance->modname.'_grade_item_update';
969
970 if (function_exists($gradefunc)) {
2b0f65e2 971
972 // legacy module - not yet converted
d185c3ee 973 if ($oldgrades = $gradefunc($modinstance->id)) {
974
975 $grademax = $oldgrades->maxgrade;
976 $scaleid = NULL;
977 if (!is_numeric($grademax)) {
978 // scale name is provided as a string, try to find it
979 if (!$scale = get_record('scale', 'name', $grademax)) {
980 debugging('Incorrect scale name! name:'.$grademax);
981 return false;
982 }
983 $scaleid = $scale->id;
984 }
985
986 if (!$grade_item = grade_get_legacy_grade_item($modinstance, $grademax, $scaleid)) {
987 debugging('Can not get/create legacy grade item!');
988 return false;
989 }
990
0b5a80a1 991 if (!empty($oldgrades->grades)) {
992 $grades = array();
d185c3ee 993
0b5a80a1 994 foreach ($oldgrades->grades as $uid=>$usergrade) {
995 if ($userid and $uid != $userid) {
996 continue;
997 }
998 $grade = new object();
999 $grade->userid = $uid;
d185c3ee 1000
0b5a80a1 1001 if ($usergrade == '-') {
1002 // no grade
1003 $grade->rawgrade = null;
d185c3ee 1004
0b5a80a1 1005 } else if ($scaleid) {
1006 // scale in use, words used
1007 $gradescale = explode(",", $scale->scale);
1008 $grade->rawgrade = array_search($usergrade, $gradescale) + 1;
1009
1010 } else {
1011 // good old numeric value
1012 $grade->rawgrade = $usergrade;
1013 }
1014 $grades[] = $grade;
d185c3ee 1015 }
d185c3ee 1016
0b5a80a1 1017 grade_update('legacygrab', $grade_item->courseid, $grade_item->itemtype, $grade_item->itemmodule,
1018 $grade_item->iteminstance, $grade_item->itemnumber, $grades);
1019 }
d185c3ee 1020 }
1021
1022 } else if (function_exists($updategradesfunc) and function_exists($updateitemfunc)) {
1023 //new grading supported, force updating of grades
1024 $updateitemfunc($modinstance);
2b0f65e2 1025 $updategradesfunc($modinstance, $userid);
d185c3ee 1026
1027 } else {
2b0f65e2 1028 // mudule does not support grading??
d185c3ee 1029 }
1030
1031 return true;
1032}
de420c11 1033
1034/**
d185c3ee 1035 * Get and update/create grade item for legacy modules.
de420c11 1036 */
1037function grade_get_legacy_grade_item($modinstance, $grademax, $scaleid) {
1038
1039 // does it already exist?
42ff9ce6 1040 if ($grade_items = grade_item::fetch_all(array('courseid'=>$modinstance->course, 'itemtype'=>'mod', 'itemmodule'=>$modinstance->modname, 'iteminstance'=>$modinstance->id, 'itemnumber'=>0))) {
de420c11 1041 if (count($grade_items) > 1) {
d185c3ee 1042 debugging('Multiple legacy grade_items found.');
de420c11 1043 return false;
1044 }
1045
1046 $grade_item = reset($grade_items);
de420c11 1047
d185c3ee 1048 if (is_null($grademax) and is_null($scaleid)) {
1049 $grade_item->gradetype = GRADE_TYPE_NONE;
de420c11 1050
d185c3ee 1051 } else if ($scaleid) {
1052 $grade_item->gradetype = GRADE_TYPE_SCALE;
1053 $grade_item->scaleid = $scaleid;
97d608ba 1054 $grade_item->grademin = 1;
de420c11 1055
d185c3ee 1056 } else {
97d608ba 1057 $grade_item->gradetype = GRADE_TYPE_VALUE;
1058 $grade_item->grademax = $grademax;
1059 $grade_item->grademin = 0;
de420c11 1060 }
1061
d185c3ee 1062 $grade_item->itemname = $modinstance->name;
1063 $grade_item->idnumber = $modinstance->cmidnumber;
de420c11 1064
d185c3ee 1065 $grade_item->update();
de420c11 1066
1067 return $grade_item;
1068 }
612607bd 1069
de420c11 1070 // create new one
d185c3ee 1071 $params = array('courseid' =>$modinstance->course,
de420c11 1072 'itemtype' =>'mod',
1073 'itemmodule' =>$modinstance->modname,
1074 'iteminstance'=>$modinstance->id,
d185c3ee 1075 'itemnumber' =>0,
de420c11 1076 'itemname' =>$modinstance->name,
1077 'idnumber' =>$modinstance->cmidnumber);
1078
d185c3ee 1079 if (is_null($grademax) and is_null($scaleid)) {
1080 $params['gradetype'] = GRADE_TYPE_NONE;
1081
1082 } else if ($scaleid) {
612607bd 1083 $params['gradetype'] = GRADE_TYPE_SCALE;
de420c11 1084 $params['scaleid'] = $scaleid;
b3ac6c3e 1085 $grade_item->grademin = 1;
de420c11 1086 } else {
612607bd 1087 $params['gradetype'] = GRADE_TYPE_VALUE;
de420c11 1088 $params['grademax'] = $grademax;
1089 $params['grademin'] = 0;
1090 }
1091
f70152b7 1092 $grade_item = new grade_item($params);
1093 $grade_item->insert();
de420c11 1094
f70152b7 1095 return $grade_item;
de420c11 1096}
1097
b51ece5b 1098/**
1099 * Remove grade letters for given context
1100 * @param object $context
1101 */
1102function remove_grade_letters($context, $showfeedback) {
1103 $strdeleted = get_string('deleted');
1104
1105 delete_records('grade_letters', 'contextid', $context->id);
1106 if ($showfeedback) {
1107 notify($strdeleted.' - '.get_string('letters', 'grades'));
1108 }
1109}
f615fbab 1110/**
1111 * Remove all grade related course data - history is kept
1112 * @param int $courseid
6b5c722d 1113 * @param bool @showfeedback print feedback
f615fbab 1114 */
1115function remove_course_grades($courseid, $showfeedback) {
1116 $strdeleted = get_string('deleted');
1117
1118 $course_category = grade_category::fetch_course_category($courseid);
1119 $course_category->delete('coursedelete');
1120 if ($showfeedback) {
1121 notify($strdeleted.' - '.get_string('grades', 'grades').', '.get_string('items', 'grades').', '.get_string('categories', 'grades'));
1122 }
1123
1124 if ($outcomes = grade_outcome::fetch_all(array('courseid'=>$courseid))) {
1125 foreach ($outcomes as $outcome) {
1126 $outcome->delete('coursedelete');
1127 }
1128 }
1129 delete_records('grade_outcomes_courses', 'courseid', $courseid);
1130 if ($showfeedback) {
1131 notify($strdeleted.' - '.get_string('outcomes', 'grades'));
1132 }
1133
1134 if ($scales = grade_scale::fetch_all(array('courseid'=>$courseid))) {
1135 foreach ($scales as $scale) {
1136 $scale->delete('coursedelete');
1137 }
1138 }
1139 if ($showfeedback) {
1140 notify($strdeleted.' - '.get_string('scales'));
1141 }
b51ece5b 1142
1143 delete_records('grade_settings', 'courseid', $courseid);
1144 if ($showfeedback) {
1145 notify($strdeleted.' - '.get_string('settings', 'grades'));
1146 }
f615fbab 1147}
bfe7297e 1148
8a0a6046 1149/**
1150 * Does gradebook cleanup when module uninstalled.
1151 */
1152function grade_uninstalled_module($modname) {
1153 global $CFG;
1154
1155 $sql = "SELECT *
1156 FROM {$CFG->prefix}grade_items
1157 WHERE itemtype='mod' AND itemmodule='$modname'";
1158
1159 // go all items for this module and delete them including the grades
1160 if ($rs = get_recordset_sql($sql)) {
1161 while ($item = rs_fetch_next_record($rs)) {
1162 $grade_item = new grade_item($item, false);
1163 $grade_item->delete('moduninstall');
1164 }
1165 rs_close($rs);
1166 }
1167}
1168
2650c51e 1169/**
1170 * Grading cron job
1171 */
1172function grade_cron() {
26101be8 1173 global $CFG;
1174
1175 $now = time();
1176
1177 $sql = "SELECT i.*
1178 FROM {$CFG->prefix}grade_items i
1179 WHERE i.locked = 0 AND i.locktime > 0 AND i.locktime < $now AND EXISTS (
1f4a0320 1180 SELECT 'x' FROM {$CFG->prefix}grade_items c WHERE c.itemtype='course' AND c.needsupdate=0 AND c.courseid=i.courseid)";
26101be8 1181
2650c51e 1182 // go through all courses that have proper final grades and lock them if needed
26101be8 1183 if ($rs = get_recordset_sql($sql)) {
03cedd62 1184 while ($item = rs_fetch_next_record($rs)) {
1185 $grade_item = new grade_item($item, false);
1186 $grade_item->locked = $now;
1187 $grade_item->update('locktime');
2650c51e 1188 }
1189 rs_close($rs);
1190 }
26101be8 1191
fcac8e51 1192 $grade_inst = new grade_grade();
1193 $fields = 'g.'.implode(',g.', $grade_inst->required_fields);
1194
1195 $sql = "SELECT $fields
26101be8 1196 FROM {$CFG->prefix}grade_grades g, {$CFG->prefix}grade_items i
1197 WHERE g.locked = 0 AND g.locktime > 0 AND g.locktime < $now AND g.itemid=i.id AND EXISTS (
1f4a0320 1198 SELECT 'x' FROM {$CFG->prefix}grade_items c WHERE c.itemtype='course' AND c.needsupdate=0 AND c.courseid=i.courseid)";
26101be8 1199
1200 // go through all courses that have proper final grades and lock them if needed
1201 if ($rs = get_recordset_sql($sql)) {
03cedd62 1202 while ($grade = rs_fetch_next_record($rs)) {
1203 $grade_grade = new grade_grade($grade, false);
1204 $grade_grade->locked = $now;
1205 $grade_grade->update('locktime');
26101be8 1206 }
1207 rs_close($rs);
1208 }
1209
1ee0df06 1210 //TODO: do not run this cleanup every cron invocation
1211 // cleanup history tables
f0362b5d 1212 if (!empty($CFG->gradehistorylifetime)) { // value in days
1213 $histlifetime = $now - ($CFG->gradehistorylifetime * 3600 * 24);
1214 $tables = array('grade_outcomes_history', 'grade_categories_history', 'grade_items_history', 'grade_grades_history', 'scale_history');
1215 foreach ($tables as $table) {
1216 if (delete_records_select($table, "timemodified < $histlifetime")) {
1217 mtrace(" Deleted old grade history records from '$table'");
1ee0df06 1218 }
1219 }
f0362b5d 1220 }
1221}
1222
1223/**
1224 * Resel all course grades
1225 * @param int $courseid
1226 * @return success
1227 */
1228function grade_course_reset($courseid) {
1229
1230 // no recalculations
1231 grade_force_full_regrading($courseid);
1232
1233 $grade_items = grade_item::fetch_all(array('courseid'=>$courseid));
1234 foreach ($grade_items as $gid=>$grade_item) {
1235 $grade_item->delete_all_grades('reset');
1236 }
1ee0df06 1237
f0362b5d 1238 //refetch all grades
1239 grade_grab_course_grades($courseid);
1ee0df06 1240
f0362b5d 1241 // recalculate all grades
1242 grade_regrade_final_grades($courseid);
1243 return true;
2650c51e 1244}
1245
b45d8391 1246/**
1247 * Convert number to float or null
1248 * @param mixed number
1249 * @return mixed float or null
1250 */
1251function grade_floatval($number) {
1252 if (is_null($number)) {
1253 return null;
1254 }
1255 return (float)$number;
1256}
60cf7430 1257?>