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