Fix for MDL-12998 and MDL-13079 Ldap not unenrolling users correctly
[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}
4b86bb08 621
622/**
623 * Returns grade options for gradebook category menu
624 * @param int $courseid
625 * @param bool $includenew include option for new category (-1)
626 * @return array of grade categories in course
627 */
628function grade_get_categories_menu($courseid, $includenew=false) {
629 $result = array();
630 if (!$categories = grade_category::fetch_all(array('courseid'=>$courseid))) {
631 //make sure course category exists
632 if (!grade_category::fetch_course_category()) {
633 debugging('Can not create course grade category!');
634 return $result;
635 }
636 $categories = grade_category::fetch_all(array('courseid'=>$courseid));
637 }
638 foreach ($categories as $key=>$category) {
639 if ($category->is_course_category()) {
640 $result[$category->id] = get_string('uncategorised', 'grades');
641 unset($categories[$key]);
642 }
643 }
644 if ($includenew) {
645 $result[-1] = get_string('newcategory', 'grades');
646 }
647 $cats = array();
648 foreach ($categories as $category) {
649 $cats[$category->id] = $category->get_name();
650 }
651 asort($cats, SORT_LOCALE_STRING);
652
653 return ($result+$cats);
654}
e9096dc2 655
656/**
657 * Returns grade letters array used in context
658 * @param object $context object or null for defaults
659 * @return array of grade_boundary=>letter_string
660 */
661function grade_get_letters($context=null) {
662 if (empty($context)) {
284abb09 663 //default grading letters
664 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 665 }
666
667 static $cache = array();
668
669 if (array_key_exists($context->id, $cache)) {
670 return $cache[$context->id];
671 }
672
673 if (count($cache) > 100) {
674 $cache = array(); // cache size limit
675 }
676
677 $letters = array();
678
679 $contexts = get_parent_contexts($context);
680 array_unshift($contexts, $context->id);
681
682 foreach ($contexts as $ctxid) {
284abb09 683 if ($records = get_records('grade_letters', 'contextid', $ctxid, 'lowerboundary DESC')) {
e9096dc2 684 foreach ($records as $record) {
284abb09 685 $letters[$record->lowerboundary] = $record->letter;
e9096dc2 686 }
687 }
688
689 if (!empty($letters)) {
690 $cache[$context->id] = $letters;
691 return $letters;
692 }
693 }
694
695 $letters = grade_get_letters(null);
696 $cache[$context->id] = $letters;
697 return $letters;
698}
699
60243313 700
701/**
2c5e52e2 702 * Verify new value of idnumber - checks for uniqueness of new idnumbers, old are kept intact
60243313 703 * @param string idnumber string (with magic quotes)
204175c5 704 * @param int $courseid - id numbers are course unique only
60243313 705 * @param object $cm used for course module idnumbers and items attached to modules
706 * @param object $gradeitem is item idnumber
707 * @return boolean true means idnumber ok
708 */
204175c5 709function grade_verify_idnumber($idnumber, $courseid, $grade_item=null, $cm=null) {
60243313 710 if ($idnumber == '') {
711 //we allow empty idnumbers
712 return true;
713 }
714
715 // keep existing even when not unique
716 if ($cm and $cm->idnumber == $idnumber) {
717 return true;
718 } else if ($grade_item and $grade_item->idnumber == $idnumber) {
719 return true;
720 }
721
204175c5 722 if (get_records_select('course_modules', "course = $courseid AND idnumber='$idnumber'")) {
60243313 723 return false;
724 }
725
204175c5 726 if (get_records_select('grade_items', "courseid = $courseid AND idnumber='$idnumber'")) {
60243313 727 return false;
728 }
729
730 return true;
731}
732
733/**
734 * Force final grade recalculation in all course items
735 * @param int $courseid
736 */
f8e6e4db 737function grade_force_full_regrading($courseid) {
738 set_field('grade_items', 'needsupdate', 1, 'courseid', $courseid);
739}
34e67f76 740
5834dcdb 741/**
ac9b0805 742 * Updates all final grades in course.
a8995b34 743 *
744 * @param int $courseid
f8e6e4db 745 * @param int $userid if specified, try to do a quick regrading of grades of this user only
746 * @param object $updated_item the item in which
747 * @return boolean true if ok, array of errors if problems found (item id is used as key)
a8995b34 748 */
c86caae7 749function grade_regrade_final_grades($courseid, $userid=null, $updated_item=null) {
b8ff92b6 750
514a3467 751 $course_item = grade_item::fetch_course_item($courseid);
f04873a9 752
f8e6e4db 753 if ($userid) {
754 // one raw grade updated for one user
755 if (empty($updated_item)) {
756 error("updated_item_id can not be null!");
757 }
758 if ($course_item->needsupdate) {
759 $updated_item->force_regrading();
760 return 'Can not do fast regrading after updating of raw grades';
a8995b34 761 }
772ddfbf 762
f8e6e4db 763 } else {
764 if (!$course_item->needsupdate) {
765 // nothing to do :-)
b8ff92b6 766 return true;
b8ff92b6 767 }
a8995b34 768 }
769
f8e6e4db 770 $grade_items = grade_item::fetch_all(array('courseid'=>$courseid));
771 $depends_on = array();
772
773 // first mark all category and calculated items as needing regrading
fb0e3570 774 // this is slower, but 100% accurate
f8e6e4db 775 foreach ($grade_items as $gid=>$gitem) {
fb46b5b6 776 if (!empty($updated_item) and $updated_item->id == $gid) {
f8e6e4db 777 $grade_items[$gid]->needsupdate = 1;
778
eacd3700 779 } else if ($gitem->is_course_item() or $gitem->is_category_item() or $gitem->is_calculated()) {
f8e6e4db 780 $grade_items[$gid]->needsupdate = 1;
781 }
2e53372c 782
f8e6e4db 783 // construct depends_on lookup array
784 $depends_on[$gid] = $grade_items[$gid]->depends_on();
785 }
2e53372c 786
d14ae855 787 $errors = array();
b8ff92b6 788 $finalids = array();
d14ae855 789 $gids = array_keys($grade_items);
eacd3700 790 $failed = 0;
d14ae855 791
792 while (count($finalids) < count($gids)) { // work until all grades are final or error found
793 $count = 0;
794 foreach ($gids as $gid) {
795 if (in_array($gid, $finalids)) {
796 continue; // already final
797 }
798
799 if (!$grade_items[$gid]->needsupdate) {
800 $finalids[] = $gid; // we can make it final - does not need update
b8ff92b6 801 continue;
802 }
803
b8ff92b6 804 $doupdate = true;
f8e6e4db 805 foreach ($depends_on[$gid] as $did) {
b8ff92b6 806 if (!in_array($did, $finalids)) {
807 $doupdate = false;
d14ae855 808 continue; // this item depends on something that is not yet in finals array
b8ff92b6 809 }
810 }
811
812 //oki - let's update, calculate or aggregate :-)
813 if ($doupdate) {
d14ae855 814 $result = $grade_items[$gid]->regrade_final_grades($userid);
f8e6e4db 815
816 if ($result === true) {
d14ae855 817 $grade_items[$gid]->regrading_finished();
fb0e3570 818 $grade_items[$gid]->check_locktime(); // do the locktime item locking
f8e6e4db 819 $count++;
b8ff92b6 820 $finalids[] = $gid;
fb0e3570 821
f8e6e4db 822 } else {
d14ae855 823 $grade_items[$gid]->force_regrading();
f8e6e4db 824 $errors[$gid] = $result;
b8ff92b6 825 }
826 }
827 }
828
829 if ($count == 0) {
eacd3700 830 $failed++;
831 } else {
832 $failed = 0;
833 }
834
835 if ($failed > 1) {
d14ae855 836 foreach($gids as $gid) {
837 if (in_array($gid, $finalids)) {
838 continue; // this one is ok
839 }
840 $grade_items[$gid]->force_regrading();
841 $errors[$grade_items[$gid]->id] = 'Probably circular reference or broken calculation formula'; // TODO: localize
b8ff92b6 842 }
d14ae855 843 break; // oki, found error
b8ff92b6 844 }
845 }
846
847 if (count($errors) == 0) {
fb0e3570 848 if (empty($userid)) {
849 // do the locktime locking of grades, but only when doing full regrading
fed7cdc9 850 grade_grade::check_locktime_all($gids);
fb0e3570 851 }
b8ff92b6 852 return true;
853 } else {
854 return $errors;
855 }
a8995b34 856}
967f222f 857
de420c11 858/**
d185c3ee 859 * For backwards compatibility with old third-party modules, this function can
860 * be used to import all grades from activities with legacy grading.
f0362b5d 861 * @param int $courseid
967f222f 862 */
f0362b5d 863function grade_grab_legacy_grades($courseid) {
ac9b0805 864 global $CFG;
967f222f 865
866 if (!$mods = get_list_of_plugins('mod') ) {
867 error('No modules installed!');
868 }
869
870 foreach ($mods as $mod) {
967f222f 871 if ($mod == 'NEWMODULE') { // Someone has unzipped the template, ignore it
872 continue;
873 }
874
d185c3ee 875 $fullmod = $CFG->dirroot.'/mod/'.$mod;
967f222f 876
877 // include the module lib once
878 if (file_exists($fullmod.'/lib.php')) {
879 include_once($fullmod.'/lib.php');
de420c11 880 // look for modname_grades() function - old gradebook pulling function
881 // if present sync the grades with new grading system
967f222f 882 $gradefunc = $mod.'_grades';
de420c11 883 if (function_exists($gradefunc)) {
f0362b5d 884 grade_grab_course_grades($courseid, $mod);
967f222f 885 }
886 }
887 }
888}
889
ac9b0805 890/**
f0362b5d 891 * Refetches data from all course activities
892 * @param int $courseid
893 * @param string $modname
894 * @return success
ac9b0805 895 */
f0362b5d 896function grade_grab_course_grades($courseid, $modname=null) {
ac9b0805 897 global $CFG;
898
f0362b5d 899 if ($modname) {
900 $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname
901 FROM {$CFG->prefix}$modname a, {$CFG->prefix}course_modules cm, {$CFG->prefix}modules m
902 WHERE m.name='$modname' AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=$courseid";
903
904 if ($modinstances = get_records_sql($sql)) {
905 foreach ($modinstances as $modinstance) {
906 grade_update_mod_grades($modinstance);
907 }
908 }
909 return;
910 }
911
ac9b0805 912 if (!$mods = get_list_of_plugins('mod') ) {
913 error('No modules installed!');
914 }
915
916 foreach ($mods as $mod) {
ac9b0805 917 if ($mod == 'NEWMODULE') { // Someone has unzipped the template, ignore it
918 continue;
919 }
920
ac9b0805 921 $fullmod = $CFG->dirroot.'/mod/'.$mod;
922
923 // include the module lib once
924 if (file_exists($fullmod.'/lib.php')) {
f0362b5d 925 // get all instance of the activity
926 $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname
927 FROM {$CFG->prefix}$mod a, {$CFG->prefix}course_modules cm, {$CFG->prefix}modules m
928 WHERE m.name='$mod' AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=$courseid";
929
930 if ($modinstances = get_records_sql($sql)) {
931 foreach ($modinstances as $modinstance) {
932 grade_update_mod_grades($modinstance);
933 }
ac9b0805 934 }
935 }
936 }
937}
938
d185c3ee 939/**
940 * Force full update of module grades in central gradebook - works for both legacy and converted activities.
941 * @param object $modinstance object with extra cmidnumber and modname property
942 * @return boolean success
943 */
2b0f65e2 944function grade_update_mod_grades($modinstance, $userid=0) {
d185c3ee 945 global $CFG;
946
947 $fullmod = $CFG->dirroot.'/mod/'.$modinstance->modname;
948 if (!file_exists($fullmod.'/lib.php')) {
949 debugging('missing lib.php file in module');
950 return false;
951 }
952 include_once($fullmod.'/lib.php');
953
954 // does it use legacy grading?
955 $gradefunc = $modinstance->modname.'_grades';
956 $updategradesfunc = $modinstance->modname.'_update_grades';
957 $updateitemfunc = $modinstance->modname.'_grade_item_update';
958
959 if (function_exists($gradefunc)) {
2b0f65e2 960
961 // legacy module - not yet converted
d185c3ee 962 if ($oldgrades = $gradefunc($modinstance->id)) {
963
964 $grademax = $oldgrades->maxgrade;
965 $scaleid = NULL;
966 if (!is_numeric($grademax)) {
967 // scale name is provided as a string, try to find it
968 if (!$scale = get_record('scale', 'name', $grademax)) {
969 debugging('Incorrect scale name! name:'.$grademax);
970 return false;
971 }
972 $scaleid = $scale->id;
973 }
974
975 if (!$grade_item = grade_get_legacy_grade_item($modinstance, $grademax, $scaleid)) {
976 debugging('Can not get/create legacy grade item!');
977 return false;
978 }
979
0b5a80a1 980 if (!empty($oldgrades->grades)) {
981 $grades = array();
d185c3ee 982
0b5a80a1 983 foreach ($oldgrades->grades as $uid=>$usergrade) {
984 if ($userid and $uid != $userid) {
985 continue;
986 }
987 $grade = new object();
988 $grade->userid = $uid;
d185c3ee 989
0b5a80a1 990 if ($usergrade == '-') {
991 // no grade
992 $grade->rawgrade = null;
d185c3ee 993
0b5a80a1 994 } else if ($scaleid) {
995 // scale in use, words used
996 $gradescale = explode(",", $scale->scale);
997 $grade->rawgrade = array_search($usergrade, $gradescale) + 1;
998
999 } else {
1000 // good old numeric value
1001 $grade->rawgrade = $usergrade;
1002 }
1003 $grades[] = $grade;
d185c3ee 1004 }
d185c3ee 1005
0b5a80a1 1006 grade_update('legacygrab', $grade_item->courseid, $grade_item->itemtype, $grade_item->itemmodule,
1007 $grade_item->iteminstance, $grade_item->itemnumber, $grades);
1008 }
d185c3ee 1009 }
1010
1011 } else if (function_exists($updategradesfunc) and function_exists($updateitemfunc)) {
1012 //new grading supported, force updating of grades
1013 $updateitemfunc($modinstance);
2b0f65e2 1014 $updategradesfunc($modinstance, $userid);
d185c3ee 1015
1016 } else {
2b0f65e2 1017 // mudule does not support grading??
d185c3ee 1018 }
1019
1020 return true;
1021}
de420c11 1022
1023/**
d185c3ee 1024 * Get and update/create grade item for legacy modules.
de420c11 1025 */
1026function grade_get_legacy_grade_item($modinstance, $grademax, $scaleid) {
1027
1028 // does it already exist?
42ff9ce6 1029 if ($grade_items = grade_item::fetch_all(array('courseid'=>$modinstance->course, 'itemtype'=>'mod', 'itemmodule'=>$modinstance->modname, 'iteminstance'=>$modinstance->id, 'itemnumber'=>0))) {
de420c11 1030 if (count($grade_items) > 1) {
d185c3ee 1031 debugging('Multiple legacy grade_items found.');
de420c11 1032 return false;
1033 }
1034
1035 $grade_item = reset($grade_items);
de420c11 1036
d185c3ee 1037 if (is_null($grademax) and is_null($scaleid)) {
1038 $grade_item->gradetype = GRADE_TYPE_NONE;
de420c11 1039
d185c3ee 1040 } else if ($scaleid) {
1041 $grade_item->gradetype = GRADE_TYPE_SCALE;
1042 $grade_item->scaleid = $scaleid;
97d608ba 1043 $grade_item->grademin = 1;
de420c11 1044
d185c3ee 1045 } else {
97d608ba 1046 $grade_item->gradetype = GRADE_TYPE_VALUE;
1047 $grade_item->grademax = $grademax;
1048 $grade_item->grademin = 0;
de420c11 1049 }
1050
d185c3ee 1051 $grade_item->itemname = $modinstance->name;
1052 $grade_item->idnumber = $modinstance->cmidnumber;
de420c11 1053
d185c3ee 1054 $grade_item->update();
de420c11 1055
1056 return $grade_item;
1057 }
612607bd 1058
de420c11 1059 // create new one
d185c3ee 1060 $params = array('courseid' =>$modinstance->course,
de420c11 1061 'itemtype' =>'mod',
1062 'itemmodule' =>$modinstance->modname,
1063 'iteminstance'=>$modinstance->id,
d185c3ee 1064 'itemnumber' =>0,
de420c11 1065 'itemname' =>$modinstance->name,
1066 'idnumber' =>$modinstance->cmidnumber);
1067
d185c3ee 1068 if (is_null($grademax) and is_null($scaleid)) {
1069 $params['gradetype'] = GRADE_TYPE_NONE;
1070
1071 } else if ($scaleid) {
612607bd 1072 $params['gradetype'] = GRADE_TYPE_SCALE;
de420c11 1073 $params['scaleid'] = $scaleid;
b3ac6c3e 1074 $grade_item->grademin = 1;
de420c11 1075 } else {
612607bd 1076 $params['gradetype'] = GRADE_TYPE_VALUE;
de420c11 1077 $params['grademax'] = $grademax;
1078 $params['grademin'] = 0;
1079 }
1080
f70152b7 1081 $grade_item = new grade_item($params);
1082 $grade_item->insert();
de420c11 1083
f70152b7 1084 return $grade_item;
de420c11 1085}
1086
b51ece5b 1087/**
1088 * Remove grade letters for given context
1089 * @param object $context
1090 */
1091function remove_grade_letters($context, $showfeedback) {
1092 $strdeleted = get_string('deleted');
1093
1094 delete_records('grade_letters', 'contextid', $context->id);
1095 if ($showfeedback) {
1096 notify($strdeleted.' - '.get_string('letters', 'grades'));
1097 }
1098}
f615fbab 1099/**
1100 * Remove all grade related course data - history is kept
1101 * @param int $courseid
6b5c722d 1102 * @param bool @showfeedback print feedback
f615fbab 1103 */
1104function remove_course_grades($courseid, $showfeedback) {
1105 $strdeleted = get_string('deleted');
1106
1107 $course_category = grade_category::fetch_course_category($courseid);
1108 $course_category->delete('coursedelete');
1109 if ($showfeedback) {
1110 notify($strdeleted.' - '.get_string('grades', 'grades').', '.get_string('items', 'grades').', '.get_string('categories', 'grades'));
1111 }
1112
1113 if ($outcomes = grade_outcome::fetch_all(array('courseid'=>$courseid))) {
1114 foreach ($outcomes as $outcome) {
1115 $outcome->delete('coursedelete');
1116 }
1117 }
1118 delete_records('grade_outcomes_courses', 'courseid', $courseid);
1119 if ($showfeedback) {
1120 notify($strdeleted.' - '.get_string('outcomes', 'grades'));
1121 }
1122
1123 if ($scales = grade_scale::fetch_all(array('courseid'=>$courseid))) {
1124 foreach ($scales as $scale) {
1125 $scale->delete('coursedelete');
1126 }
1127 }
1128 if ($showfeedback) {
1129 notify($strdeleted.' - '.get_string('scales'));
1130 }
b51ece5b 1131
1132 delete_records('grade_settings', 'courseid', $courseid);
1133 if ($showfeedback) {
1134 notify($strdeleted.' - '.get_string('settings', 'grades'));
1135 }
f615fbab 1136}
bfe7297e 1137
2650c51e 1138/**
1139 * Grading cron job
1140 */
1141function grade_cron() {
26101be8 1142 global $CFG;
1143
1144 $now = time();
1145
1146 $sql = "SELECT i.*
1147 FROM {$CFG->prefix}grade_items i
1148 WHERE i.locked = 0 AND i.locktime > 0 AND i.locktime < $now AND EXISTS (
1f4a0320 1149 SELECT 'x' FROM {$CFG->prefix}grade_items c WHERE c.itemtype='course' AND c.needsupdate=0 AND c.courseid=i.courseid)";
26101be8 1150
2650c51e 1151 // go through all courses that have proper final grades and lock them if needed
26101be8 1152 if ($rs = get_recordset_sql($sql)) {
03cedd62 1153 while ($item = rs_fetch_next_record($rs)) {
1154 $grade_item = new grade_item($item, false);
1155 $grade_item->locked = $now;
1156 $grade_item->update('locktime');
2650c51e 1157 }
1158 rs_close($rs);
1159 }
26101be8 1160
fcac8e51 1161 $grade_inst = new grade_grade();
1162 $fields = 'g.'.implode(',g.', $grade_inst->required_fields);
1163
1164 $sql = "SELECT $fields
26101be8 1165 FROM {$CFG->prefix}grade_grades g, {$CFG->prefix}grade_items i
1166 WHERE g.locked = 0 AND g.locktime > 0 AND g.locktime < $now AND g.itemid=i.id AND EXISTS (
1f4a0320 1167 SELECT 'x' FROM {$CFG->prefix}grade_items c WHERE c.itemtype='course' AND c.needsupdate=0 AND c.courseid=i.courseid)";
26101be8 1168
1169 // go through all courses that have proper final grades and lock them if needed
1170 if ($rs = get_recordset_sql($sql)) {
03cedd62 1171 while ($grade = rs_fetch_next_record($rs)) {
1172 $grade_grade = new grade_grade($grade, false);
1173 $grade_grade->locked = $now;
1174 $grade_grade->update('locktime');
26101be8 1175 }
1176 rs_close($rs);
1177 }
1178
1ee0df06 1179 //TODO: do not run this cleanup every cron invocation
1180 // cleanup history tables
f0362b5d 1181 if (!empty($CFG->gradehistorylifetime)) { // value in days
1182 $histlifetime = $now - ($CFG->gradehistorylifetime * 3600 * 24);
1183 $tables = array('grade_outcomes_history', 'grade_categories_history', 'grade_items_history', 'grade_grades_history', 'scale_history');
1184 foreach ($tables as $table) {
1185 if (delete_records_select($table, "timemodified < $histlifetime")) {
1186 mtrace(" Deleted old grade history records from '$table'");
1ee0df06 1187 }
1188 }
f0362b5d 1189 }
1190}
1191
1192/**
1193 * Resel all course grades
1194 * @param int $courseid
1195 * @return success
1196 */
1197function grade_course_reset($courseid) {
1198
1199 // no recalculations
1200 grade_force_full_regrading($courseid);
1201
1202 $grade_items = grade_item::fetch_all(array('courseid'=>$courseid));
1203 foreach ($grade_items as $gid=>$grade_item) {
1204 $grade_item->delete_all_grades('reset');
1205 }
1ee0df06 1206
f0362b5d 1207 //refetch all grades
1208 grade_grab_course_grades($courseid);
1ee0df06 1209
f0362b5d 1210 // recalculate all grades
1211 grade_regrade_final_grades($courseid);
1212 return true;
2650c51e 1213}
1214
60cf7430 1215?>