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