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