weekly release 2.3dev
[moodle.git] / lib / grade / grade_item.php
CommitLineData
4a0e2e63 1<?php
3c2e81ee 2
7ad5a627
PS
3// This file is part of Moodle - http://moodle.org/
4//
5// Moodle is free software: you can redistribute it and/or modify
6// it under the terms of the GNU General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// Moodle is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17/**
18 * Definitions of grade item class
19 *
20 * @package core
21 * @subpackage grade
22 * @copyright 2006 Nicolas Connault
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24 */
3c2e81ee 25
7ad5a627 26defined('MOODLE_INTERNAL') || die();
3c2e81ee 27require_once('grade_object.php');
4ac209d5 28
3c2e81ee 29/**
30 * Class representing a grade item. It is responsible for handling its DB representation,
31 * modifying and returning its metadata.
32 */
33class grade_item extends grade_object {
34 /**
35 * DB Table (used by grade_object).
36 * @var string $table
37 */
da3801e8 38 public $table = 'grade_items';
3c2e81ee 39
40 /**
41 * Array of required table fields, must start with 'id'.
42 * @var array $required_fields
43 */
da3801e8 44 public $required_fields = array('id', 'courseid', 'categoryid', 'itemname', 'itemtype', 'itemmodule', 'iteminstance',
3c2e81ee 45 'itemnumber', 'iteminfo', 'idnumber', 'calculation', 'gradetype', 'grademax', 'grademin',
46 'scaleid', 'outcomeid', 'gradepass', 'multfactor', 'plusfactor', 'aggregationcoef',
47 'sortorder', 'display', 'decimals', 'hidden', 'locked', 'locktime', 'needsupdate', 'timecreated',
48 'timemodified');
49
50 /**
51 * The course this grade_item belongs to.
52 * @var int $courseid
53 */
da3801e8 54 public $courseid;
3c2e81ee 55
56 /**
57 * The category this grade_item belongs to (optional).
58 * @var int $categoryid
59 */
da3801e8 60 public $categoryid;
3c2e81ee 61
62 /**
63 * The grade_category object referenced $this->iteminstance (itemtype must be == 'category' or == 'course' in that case).
64 * @var object $item_category
65 */
da3801e8 66 public $item_category;
3c2e81ee 67
68 /**
69 * The grade_category object referenced by $this->categoryid.
70 * @var object $parent_category
71 */
da3801e8 72 public $parent_category;
3c2e81ee 73
74
75 /**
76 * The name of this grade_item (pushed by the module).
77 * @var string $itemname
78 */
da3801e8 79 public $itemname;
3c2e81ee 80
81 /**
82 * e.g. 'category', 'course' and 'mod', 'blocks', 'import', etc...
83 * @var string $itemtype
84 */
da3801e8 85 public $itemtype;
3c2e81ee 86
87 /**
88 * The module pushing this grade (e.g. 'forum', 'quiz', 'assignment' etc).
89 * @var string $itemmodule
90 */
da3801e8 91 public $itemmodule;
3c2e81ee 92
93 /**
94 * ID of the item module
95 * @var int $iteminstance
96 */
da3801e8 97 public $iteminstance;
3c2e81ee 98
99 /**
100 * Number of the item in a series of multiple grades pushed by an activity.
101 * @var int $itemnumber
102 */
da3801e8 103 public $itemnumber;
3c2e81ee 104
105 /**
106 * Info and notes about this item.
107 * @var string $iteminfo
108 */
da3801e8 109 public $iteminfo;
3c2e81ee 110
111 /**
112 * Arbitrary idnumber provided by the module responsible.
113 * @var string $idnumber
114 */
da3801e8 115 public $idnumber;
3c2e81ee 116
117 /**
118 * Calculation string used for this item.
119 * @var string $calculation
120 */
da3801e8 121 public $calculation;
3c2e81ee 122
123 /**
124 * Indicates if we already tried to normalize the grade calculation formula.
125 * This flag helps to minimize db access when broken formulas used in calculation.
126 * @var boolean
127 */
da3801e8 128 public $calculation_normalized;
3c2e81ee 129 /**
130 * Math evaluation object
131 */
da3801e8 132 public $formula;
3c2e81ee 133
134 /**
135 * The type of grade (0 = none, 1 = value, 2 = scale, 3 = text)
136 * @var int $gradetype
137 */
da3801e8 138 public $gradetype = GRADE_TYPE_VALUE;
3c2e81ee 139
140 /**
141 * Maximum allowable grade.
142 * @var float $grademax
143 */
da3801e8 144 public $grademax = 100;
3c2e81ee 145
146 /**
147 * Minimum allowable grade.
148 * @var float $grademin
149 */
da3801e8 150 public $grademin = 0;
3c2e81ee 151
152 /**
153 * id of the scale, if this grade is based on a scale.
154 * @var int $scaleid
155 */
da3801e8 156 public $scaleid;
3c2e81ee 157
158 /**
159 * A grade_scale object (referenced by $this->scaleid).
160 * @var object $scale
161 */
da3801e8 162 public $scale;
3c2e81ee 163
164 /**
165 * The id of the optional grade_outcome associated with this grade_item.
166 * @var int $outcomeid
167 */
da3801e8 168 public $outcomeid;
3c2e81ee 169
170 /**
171 * The grade_outcome this grade is associated with, if applicable.
172 * @var object $outcome
173 */
da3801e8 174 public $outcome;
3c2e81ee 175
176 /**
177 * grade required to pass. (grademin <= gradepass <= grademax)
178 * @var float $gradepass
179 */
da3801e8 180 public $gradepass = 0;
3c2e81ee 181
182 /**
183 * Multiply all grades by this number.
184 * @var float $multfactor
185 */
da3801e8 186 public $multfactor = 1.0;
3c2e81ee 187
188 /**
189 * Add this to all grades.
190 * @var float $plusfactor
191 */
da3801e8 192 public $plusfactor = 0;
3c2e81ee 193
194 /**
195 * Aggregation coeficient used for weighted averages
196 * @var float $aggregationcoef
197 */
da3801e8 198 public $aggregationcoef = 0;
3c2e81ee 199
200 /**
201 * Sorting order of the columns.
202 * @var int $sortorder
203 */
da3801e8 204 public $sortorder = 0;
3c2e81ee 205
206 /**
207 * Display type of the grades (Real, Percentage, Letter, or default).
208 * @var int $display
209 */
da3801e8 210 public $display = GRADE_DISPLAY_TYPE_DEFAULT;
3c2e81ee 211
212 /**
213 * The number of digits after the decimal point symbol. Applies only to REAL and PERCENTAGE grade display types.
214 * @var int $decimals
215 */
da3801e8 216 public $decimals = null;
3c2e81ee 217
3c2e81ee 218 /**
219 * Grade item lock flag. Empty if not locked, locked if any value present, usually date when item was locked. Locking prevents updating.
220 * @var int $locked
221 */
da3801e8 222 public $locked = 0;
3c2e81ee 223
224 /**
225 * Date after which the grade will be locked. Empty means no automatic locking.
226 * @var int $locktime
227 */
da3801e8 228 public $locktime = 0;
3c2e81ee 229
230 /**
231 * If set, the whole column will be recalculated, then this flag will be switched off.
232 * @var boolean $needsupdate
233 */
da3801e8 234 public $needsupdate = 1;
3c2e81ee 235
236 /**
237 * Cached dependson array
238 */
da3801e8 239 public $dependson_cache = null;
3c2e81ee 240
241 /**
242 * In addition to update() as defined in grade_object, handle the grade_outcome and grade_scale objects.
25bcd908 243 * Force regrading if necessary, rounds the float numbers using php function,
244 * the reason is we need to compare the db value with computed number to skip regrading if possible.
3c2e81ee 245 * @param string $source from where was the object inserted (mod/forum, manual, etc.)
246 * @return boolean success
247 */
da3801e8 248 public function update($source=null) {
3c2e81ee 249 // reset caches
250 $this->dependson_cache = null;
251
252 // Retrieve scale and infer grademax/min from it if needed
253 $this->load_scale();
254
255 // make sure there is not 0 in outcomeid
256 if (empty($this->outcomeid)) {
257 $this->outcomeid = null;
258 }
259
260 if ($this->qualifies_for_regrading()) {
261 $this->force_regrading();
262 }
263
ced5ee59 264 $this->timemodified = time();
265
25bcd908 266 $this->grademin = grade_floatval($this->grademin);
267 $this->grademax = grade_floatval($this->grademax);
268 $this->multfactor = grade_floatval($this->multfactor);
269 $this->plusfactor = grade_floatval($this->plusfactor);
270 $this->aggregationcoef = grade_floatval($this->aggregationcoef);
271
3c2e81ee 272 return parent::update($source);
273 }
274
275 /**
276 * Compares the values held by this object with those of the matching record in DB, and returns
277 * whether or not these differences are sufficient to justify an update of all parent objects.
278 * This assumes that this object has an id number and a matching record in DB. If not, it will return false.
279 * @return boolean
280 */
da3801e8 281 public function qualifies_for_regrading() {
3c2e81ee 282 if (empty($this->id)) {
283 return false;
284 }
285
f3ac8eb4 286 $db_item = new grade_item(array('id' => $this->id));
287
288 $calculationdiff = $db_item->calculation != $this->calculation;
289 $categorydiff = $db_item->categoryid != $this->categoryid;
290 $gradetypediff = $db_item->gradetype != $this->gradetype;
f3ac8eb4 291 $scaleiddiff = $db_item->scaleid != $this->scaleid;
292 $outcomeiddiff = $db_item->outcomeid != $this->outcomeid;
f3ac8eb4 293 $locktimediff = $db_item->locktime != $this->locktime;
25bcd908 294 $grademindiff = grade_floats_different($db_item->grademin, $this->grademin);
295 $grademaxdiff = grade_floats_different($db_item->grademax, $this->grademax);
296 $multfactordiff = grade_floats_different($db_item->multfactor, $this->multfactor);
297 $plusfactordiff = grade_floats_different($db_item->plusfactor, $this->plusfactor);
298 $acoefdiff = grade_floats_different($db_item->aggregationcoef, $this->aggregationcoef);
3c2e81ee 299
300 $needsupdatediff = !$db_item->needsupdate && $this->needsupdate; // force regrading only if setting the flag first time
301 $lockeddiff = !empty($db_item->locked) && empty($this->locked); // force regrading only when unlocking
302
303 return ($calculationdiff || $categorydiff || $gradetypediff || $grademaxdiff || $grademindiff || $scaleiddiff
304 || $outcomeiddiff || $multfactordiff || $plusfactordiff || $needsupdatediff
305 || $lockeddiff || $acoefdiff || $locktimediff);
306 }
307
308 /**
309 * Finds and returns a grade_item instance based on params.
310 * @static
311 *
312 * @param array $params associative arrays varname=>value
313 * @return object grade_item instance or false if none found.
314 */
da3801e8 315 public static function fetch($params) {
f3ac8eb4 316 return grade_object::fetch_helper('grade_items', 'grade_item', $params);
3c2e81ee 317 }
318
319 /**
320 * Finds and returns all grade_item instances based on params.
321 * @static
322 *
323 * @param array $params associative arrays varname=>value
992cfb11 324 * @return array array of grade_item instances or false if none found.
3c2e81ee 325 */
da3801e8 326 public static function fetch_all($params) {
f3ac8eb4 327 return grade_object::fetch_all_helper('grade_items', 'grade_item', $params);
3c2e81ee 328 }
329
330 /**
331 * Delete all grades and force_regrading of parent category.
332 * @param string $source from where was the object deleted (mod/forum, manual, etc.)
333 * @return boolean success
334 */
da3801e8 335 public function delete($source=null) {
f0362b5d 336 $this->delete_all_grades($source);
337 return parent::delete($source);
338 }
339
340 /**
341 * Delete all grades
342 * @param string $source from where was the object deleted (mod/forum, manual, etc.)
343 * @return boolean success
344 */
da3801e8 345 public function delete_all_grades($source=null) {
3c2e81ee 346 if (!$this->is_course_item()) {
347 $this->force_regrading();
348 }
4ac209d5 349
f3ac8eb4 350 if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
3c2e81ee 351 foreach ($grades as $grade) {
352 $grade->delete($source);
353 }
354 }
355
f0362b5d 356 return true;
3c2e81ee 357 }
358
359 /**
360 * In addition to perform parent::insert(), calls force_regrading() method too.
361 * @param string $source from where was the object inserted (mod/forum, manual, etc.)
362 * @return int PK ID if successful, false otherwise
363 */
da3801e8 364 public function insert($source=null) {
365 global $CFG, $DB;
3c2e81ee 366
367 if (empty($this->courseid)) {
2f137aa1 368 print_error('cannotinsertgrade');
3c2e81ee 369 }
370
371 // load scale if needed
372 $this->load_scale();
373
374 // add parent category if needed
375 if (empty($this->categoryid) and !$this->is_course_item() and !$this->is_category_item()) {
f3ac8eb4 376 $course_category = grade_category::fetch_course_category($this->courseid);
3c2e81ee 377 $this->categoryid = $course_category->id;
378
379 }
380
381 // always place the new items at the end, move them after insert if needed
9718765e 382 $last_sortorder = $DB->get_field_select('grade_items', 'MAX(sortorder)', "courseid = ?", array($this->courseid));
3c2e81ee 383 if (!empty($last_sortorder)) {
384 $this->sortorder = $last_sortorder + 1;
385 } else {
386 $this->sortorder = 1;
387 }
388
389 // add proper item numbers to manual items
390 if ($this->itemtype == 'manual') {
391 if (empty($this->itemnumber)) {
392 $this->itemnumber = 0;
393 }
394 }
395
396 // make sure there is not 0 in outcomeid
397 if (empty($this->outcomeid)) {
398 $this->outcomeid = null;
399 }
400
ced5ee59 401 $this->timecreated = $this->timemodified = time();
402
3c2e81ee 403 if (parent::insert($source)) {
404 // force regrading of items if needed
405 $this->force_regrading();
406 return $this->id;
407
408 } else {
409 debugging("Could not insert this grade_item in the database!");
410 return false;
411 }
412 }
413
414 /**
415 * Set idnumber of grade item, updates also course_modules table
416 * @param string $idnumber (without magic quotes)
417 * @return boolean success
418 */
da3801e8 419 public function add_idnumber($idnumber) {
420 global $DB;
3c2e81ee 421 if (!empty($this->idnumber)) {
422 return false;
423 }
424
425 if ($this->itemtype == 'mod' and !$this->is_outcome_item()) {
f829c8d0
DM
426 if ($this->itemnumber === 0) {
427 // for activity modules, itemnumber 0 is synced with the course_modules
428 if (!$cm = get_coursemodule_from_instance($this->itemmodule, $this->iteminstance, $this->courseid)) {
429 return false;
430 }
431 if (!empty($cm->idnumber)) {
432 return false;
433 }
f685e830
PS
434 $DB->set_field('course_modules', 'idnumber', $idnumber, array('id' => $cm->id));
435 $this->idnumber = $idnumber;
436 return $this->update();
f829c8d0 437 } else {
3c2e81ee 438 $this->idnumber = $idnumber;
439 return $this->update();
440 }
3c2e81ee 441
442 } else {
443 $this->idnumber = $idnumber;
444 return $this->update();
445 }
446 }
447
448 /**
449 * Returns the locked state of this grade_item (if the grade_item is locked OR no specific
450 * $userid is given) or the locked state of a specific grade within this item if a specific
451 * $userid is given and the grade_item is unlocked.
452 *
453 * @param int $userid
454 * @return boolean Locked state
455 */
da3801e8 456 public function is_locked($userid=NULL) {
3c2e81ee 457 if (!empty($this->locked)) {
458 return true;
459 }
460
461 if (!empty($userid)) {
f3ac8eb4 462 if ($grade = grade_grade::fetch(array('itemid'=>$this->id, 'userid'=>$userid))) {
3c2e81ee 463 $grade->grade_item =& $this; // prevent db fetching of cached grade_item
464 return $grade->is_locked();
465 }
466 }
467
468 return false;
469 }
470
471 /**
472 * Locks or unlocks this grade_item and (optionally) all its associated final grades.
473 * @param int $locked 0, 1 or a timestamp int(10) after which date the item will be locked.
474 * @param boolean $cascade lock/unlock child objects too
475 * @param boolean $refresh refresh grades when unlocking
476 * @return boolean true if grade_item all grades updated, false if at least one update fails
477 */
da3801e8 478 public function set_locked($lockedstate, $cascade=false, $refresh=true) {
3c2e81ee 479 if ($lockedstate) {
480 /// setting lock
481 if ($this->needsupdate) {
482 return false; // can not lock grade without first having final grade
483 }
484
485 $this->locked = time();
486 $this->update();
487
488 if ($cascade) {
489 $grades = $this->get_final();
490 foreach($grades as $g) {
f3ac8eb4 491 $grade = new grade_grade($g, false);
3c2e81ee 492 $grade->grade_item =& $this;
493 $grade->set_locked(1, null, false);
494 }
495 }
496
497 return true;
498
499 } else {
500 /// removing lock
501 if (!empty($this->locked) and $this->locktime < time()) {
502 //we have to reset locktime or else it would lock up again
503 $this->locktime = 0;
504 }
505
506 $this->locked = 0;
507 $this->update();
508
509 if ($cascade) {
f3ac8eb4 510 if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
3c2e81ee 511 foreach($grades as $grade) {
512 $grade->grade_item =& $this;
513 $grade->set_locked(0, null, false);
514 }
515 }
516 }
517
518 if ($refresh) {
519 //refresh when unlocking
520 $this->refresh_grades();
521 }
522
523 return true;
524 }
525 }
526
527 /**
528 * Lock the grade if needed - make sure this is called only when final grades are valid
529 */
da3801e8 530 public function check_locktime() {
3c2e81ee 531 if (!empty($this->locked)) {
532 return; // already locked
533 }
534
535 if ($this->locktime and $this->locktime < time()) {
536 $this->locked = time();
537 $this->update('locktime');
538 }
539 }
540
541 /**
542 * Set the locktime for this grade item.
543 *
544 * @param int $locktime timestamp for lock to activate
545 * @return void
546 */
da3801e8 547 public function set_locktime($locktime) {
3c2e81ee 548 $this->locktime = $locktime;
549 $this->update();
550 }
551
552 /**
553 * Set the locktime for this grade item.
554 *
555 * @return int $locktime timestamp for lock to activate
556 */
da3801e8 557 public function get_locktime() {
3c2e81ee 558 return $this->locktime;
559 }
560
3c2e81ee 561 /**
562 * Set the hidden status of grade_item and all grades, 0 mean visible, 1 always hidden, number means date to hide until.
563 * @param int $hidden new hidden status
564 * @param boolean $cascade apply to child objects too
565 * @return void
566 */
da3801e8 567 public function set_hidden($hidden, $cascade=false) {
a25bb902 568 parent::set_hidden($hidden, $cascade);
3c2e81ee 569
570 if ($cascade) {
f3ac8eb4 571 if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
3c2e81ee 572 foreach($grades as $grade) {
573 $grade->grade_item =& $this;
574 $grade->set_hidden($hidden, $cascade);
575 }
576 }
577 }
d90aa634
AD
578
579 //if marking item visible make sure category is visible MDL-21367
580 if( !$hidden ) {
581 $category_array = grade_category::fetch_all(array('id'=>$this->categoryid));
582 if ($category_array && array_key_exists($this->categoryid, $category_array)) {
583 $category = $category_array[$this->categoryid];
584 //call set_hidden on the category regardless of whether it is hidden as its parent might be hidden
585 //if($category->is_hidden()) {
586 $category->set_hidden($hidden, false);
587 //}
588 }
589 }
3c2e81ee 590 }
591
592 /**
593 * Returns the number of grades that are hidden.
9718765e 594 * @param string $groupsql
595 * @param array $params sql params in $groupsql
596 * @param string $groupsqlwhere
597 * @return int Number of hidden grades
3c2e81ee 598 */
9718765e 599 public function has_hidden_grades($groupsql="", array $params=null, $groupwheresql="") {
5b0af8c5 600 global $DB;
9718765e 601 $params = (array)$params;
602 $params['itemid'] = $this->id;
603
5b0af8c5 604 return $DB->get_field_sql("SELECT COUNT(*) FROM {grade_grades} g LEFT JOIN "
9718765e 605 ."{user} u ON g.userid = u.id $groupsql WHERE itemid = :itemid AND hidden = 1 $groupwheresql", $params);
3c2e81ee 606 }
607
608 /**
609 * Mark regrading as finished successfully.
610 */
da3801e8 611 public function regrading_finished() {
612 global $DB;
3c2e81ee 613 $this->needsupdate = 0;
614 //do not use $this->update() because we do not want this logged in grade_item_history
da3801e8 615 $DB->set_field('grade_items', 'needsupdate', 0, array('id' => $this->id));
3c2e81ee 616 }
617
618 /**
619 * Performs the necessary calculations on the grades_final referenced by this grade_item.
620 * Also resets the needsupdate flag once successfully performed.
621 *
622 * This function must be used ONLY from lib/gradeslib.php/grade_regrade_final_grades(),
623 * because the regrading must be done in correct order!!
624 *
625 * @return boolean true if ok, error string otherwise
626 */
da3801e8 627 public function regrade_final_grades($userid=null) {
628 global $CFG, $DB;
3c2e81ee 629
630 // locked grade items already have correct final grades
631 if ($this->is_locked()) {
632 return true;
633 }
634
635 // calculation produces final value using formula from other final values
636 if ($this->is_calculated()) {
637 if ($this->compute($userid)) {
638 return true;
639 } else {
640 return "Could not calculate grades for grade item"; // TODO: improve and localize
641 }
642
643 // noncalculated outcomes already have final values - raw grades not used
644 } else if ($this->is_outcome_item()) {
645 return true;
646
647 // aggregate the category grade
648 } else if ($this->is_category_item() or $this->is_course_item()) {
649 // aggregate category grade item
650 $category = $this->get_item_category();
651 $category->grade_item =& $this;
652 if ($category->generate_grades($userid)) {
653 return true;
654 } else {
655 return "Could not aggregate final grades for category:".$this->id; // TODO: improve and localize
656 }
657
658 } else if ($this->is_manual_item()) {
659 // manual items track only final grades, no raw grades
660 return true;
661
662 } else if (!$this->is_raw_used()) {
663 // hmm - raw grades are not used- nothing to regrade
664 return true;
665 }
666
667 // normal grade item - just new final grades
668 $result = true;
f3ac8eb4 669 $grade_inst = new grade_grade();
3c2e81ee 670 $fields = implode(',', $grade_inst->required_fields);
671 if ($userid) {
5b0af8c5 672 $params = array($this->id, $userid);
673 $rs = $DB->get_recordset_select('grade_grades', "itemid=? AND userid=?", $params, '', $fields);
3c2e81ee 674 } else {
da3801e8 675 $rs = $DB->get_recordset('grade_grades', array('itemid' => $this->id), '', $fields);
3c2e81ee 676 }
677 if ($rs) {
da3801e8 678 foreach ($rs as $grade_record) {
f3ac8eb4 679 $grade = new grade_grade($grade_record, false);
3c2e81ee 680
681 if (!empty($grade_record->locked) or !empty($grade_record->overridden)) {
682 // this grade is locked - final grade must be ok
683 continue;
684 }
685
686 $grade->finalgrade = $this->adjust_raw_grade($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
687
25bcd908 688 if (grade_floats_different($grade_record->finalgrade, $grade->finalgrade)) {
3c2e81ee 689 if (!$grade->update('system')) {
690 $result = "Internal error updating final grade";
691 }
692 }
693 }
da3801e8 694 $rs->close();
3c2e81ee 695 }
696
697 return $result;
698 }
699
700 /**
701 * Given a float grade value or integer grade scale, applies a number of adjustment based on
702 * grade_item variables and returns the result.
b45d8391 703 * @param float $rawgrade The raw grade value.
704 * @param float $rawmin original rawmin
705 * @param float $rawmax original rawmax
3c2e81ee 706 * @return mixed
707 */
da3801e8 708 public function adjust_raw_grade($rawgrade, $rawmin, $rawmax) {
3c2e81ee 709 if (is_null($rawgrade)) {
710 return null;
711 }
712
713 if ($this->gradetype == GRADE_TYPE_VALUE) { // Dealing with numerical grade
714
715 if ($this->grademax < $this->grademin) {
716 return null;
717 }
718
719 if ($this->grademax == $this->grademin) {
720 return $this->grademax; // no range
721 }
722
723 // Standardise score to the new grade range
724 // NOTE: this is not compatible with current assignment grading
b45d8391 725 if ($this->itemmodule != 'assignment' and ($rawmin != $this->grademin or $rawmax != $this->grademax)) {
9a68cffc 726 $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax);
3c2e81ee 727 }
728
729 // Apply other grade_item factors
730 $rawgrade *= $this->multfactor;
731 $rawgrade += $this->plusfactor;
732
653a8648 733 return $this->bounded_grade($rawgrade);
3c2e81ee 734
735 } else if ($this->gradetype == GRADE_TYPE_SCALE) { // Dealing with a scale value
736 if (empty($this->scale)) {
737 $this->load_scale();
738 }
739
740 if ($this->grademax < 0) {
741 return null; // scale not present - no grade
742 }
743
744 if ($this->grademax == 0) {
745 return $this->grademax; // only one option
746 }
747
748 // Convert scale if needed
749 // NOTE: this is not compatible with current assignment grading
b45d8391 750 if ($this->itemmodule != 'assignment' and ($rawmin != $this->grademin or $rawmax != $this->grademax)) {
9a68cffc 751 $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax);
3c2e81ee 752 }
753
653a8648 754 return $this->bounded_grade($rawgrade);
3c2e81ee 755
756
757 } else if ($this->gradetype == GRADE_TYPE_TEXT or $this->gradetype == GRADE_TYPE_NONE) { // no value
758 // somebody changed the grading type when grades already existed
759 return null;
760
761 } else {
a7bea6c8 762 debugging("Unknown grade type");
f3ac8eb4 763 return null;
3c2e81ee 764 }
765 }
766
767 /**
768 * Sets this grade_item's needsupdate to true. Also marks the course item as needing update.
769 * @return void
770 */
da3801e8 771 public function force_regrading() {
f33e1ed4 772 global $DB;
3c2e81ee 773 $this->needsupdate = 1;
774 //mark this item and course item only - categories and calculated items are always regraded
f33e1ed4 775 $wheresql = "(itemtype='course' OR id=?) AND courseid=?";
776 $params = array($this->id, $this->courseid);
777 $DB->set_field_select('grade_items', 'needsupdate', 1, $wheresql, $params);
3c2e81ee 778 }
779
780 /**
781 * Instantiates a grade_scale object whose data is retrieved from the DB,
782 * if this item's scaleid variable is set.
783 * @return object grade_scale or null if no scale used
784 */
da3801e8 785 public function load_scale() {
3c2e81ee 786 if ($this->gradetype != GRADE_TYPE_SCALE) {
787 $this->scaleid = null;
788 }
789
790 if (!empty($this->scaleid)) {
791 //do not load scale if already present
792 if (empty($this->scale->id) or $this->scale->id != $this->scaleid) {
f3ac8eb4 793 $this->scale = grade_scale::fetch(array('id'=>$this->scaleid));
92b0d47c 794 if (!$this->scale) {
795 debugging('Incorrect scale id: '.$this->scaleid);
796 $this->scale = null;
797 return null;
798 }
3c2e81ee 799 $this->scale->load_items();
800 }
801
802 // Until scales are uniformly set to min=0 max=count(scaleitems)-1 throughout Moodle, we
803 // stay with the current min=1 max=count(scaleitems)
804 $this->grademax = count($this->scale->scale_items);
805 $this->grademin = 1;
806
807 } else {
808 $this->scale = null;
809 }
810
811 return $this->scale;
812 }
813
814 /**
815 * Instantiates a grade_outcome object whose data is retrieved from the DB,
816 * if this item's outcomeid variable is set.
817 * @return object grade_outcome
818 */
da3801e8 819 public function load_outcome() {
3c2e81ee 820 if (!empty($this->outcomeid)) {
f3ac8eb4 821 $this->outcome = grade_outcome::fetch(array('id'=>$this->outcomeid));
3c2e81ee 822 }
823 return $this->outcome;
824 }
825
826 /**
827 * Returns the grade_category object this grade_item belongs to (referenced by categoryid)
828 * or category attached to category item.
829 *
830 * @return mixed grade_category object if applicable, false if course item
831 */
da3801e8 832 public function get_parent_category() {
3c2e81ee 833 if ($this->is_category_item() or $this->is_course_item()) {
834 return $this->get_item_category();
835
836 } else {
f3ac8eb4 837 return grade_category::fetch(array('id'=>$this->categoryid));
3c2e81ee 838 }
839 }
840
841 /**
842 * Calls upon the get_parent_category method to retrieve the grade_category object
843 * from the DB and assigns it to $this->parent_category. It also returns the object.
844 * @return object Grade_category
845 */
da3801e8 846 public function load_parent_category() {
3c2e81ee 847 if (empty($this->parent_category->id)) {
848 $this->parent_category = $this->get_parent_category();
849 }
850 return $this->parent_category;
851 }
852
853 /**
854 * Returns the grade_category for category item
855 *
856 * @return mixed grade_category object if applicable, false otherwise
857 */
da3801e8 858 public function get_item_category() {
3c2e81ee 859 if (!$this->is_course_item() and !$this->is_category_item()) {
860 return false;
861 }
f3ac8eb4 862 return grade_category::fetch(array('id'=>$this->iteminstance));
3c2e81ee 863 }
864
865 /**
866 * Calls upon the get_item_category method to retrieve the grade_category object
867 * from the DB and assigns it to $this->item_category. It also returns the object.
868 * @return object Grade_category
869 */
da3801e8 870 public function load_item_category() {
79312a06 871 if (empty($this->item_category->id)) {
3c2e81ee 872 $this->item_category = $this->get_item_category();
873 }
874 return $this->item_category;
875 }
876
877 /**
878 * Is the grade item associated with category?
879 * @return boolean
880 */
da3801e8 881 public function is_category_item() {
3c2e81ee 882 return ($this->itemtype == 'category');
883 }
884
885 /**
886 * Is the grade item associated with course?
887 * @return boolean
888 */
da3801e8 889 public function is_course_item() {
3c2e81ee 890 return ($this->itemtype == 'course');
891 }
892
893 /**
f7d515b6 894 * Is this a manually graded item?
3c2e81ee 895 * @return boolean
896 */
da3801e8 897 public function is_manual_item() {
3c2e81ee 898 return ($this->itemtype == 'manual');
899 }
900
901 /**
902 * Is this an outcome item?
903 * @return boolean
904 */
da3801e8 905 public function is_outcome_item() {
3c2e81ee 906 return !empty($this->outcomeid);
907 }
908
909 /**
0f392ff4 910 * Is the grade item external - associated with module, plugin or something else?
3c2e81ee 911 * @return boolean
912 */
da3801e8 913 public function is_external_item() {
0f392ff4 914 return ($this->itemtype == 'mod');
915 }
916
917 /**
918 * Is the grade item overridable
919 * @return boolean
920 */
da3801e8 921 public function is_overridable_item() {
0f392ff4 922 return !$this->is_outcome_item() and ($this->is_external_item() or $this->is_calculated() or $this->is_course_item() or $this->is_category_item());
3c2e81ee 923 }
924
5048575d 925 /**
926 * Is the grade item feedback overridable
927 * @return boolean
928 */
da3801e8 929 public function is_overridable_item_feedback() {
5048575d 930 return !$this->is_outcome_item() and $this->is_external_item();
931 }
932
3c2e81ee 933 /**
934 * Returns true if grade items uses raw grades
935 * @return boolean
936 */
da3801e8 937 public function is_raw_used() {
0f392ff4 938 return ($this->is_external_item() and !$this->is_calculated() and !$this->is_outcome_item());
3c2e81ee 939 }
940
941 /**
942 * Returns grade item associated with the course
943 * @param int $courseid
944 * @return course item object
945 */
22a9b6d8 946 public static function fetch_course_item($courseid) {
f3ac8eb4 947 if ($course_item = grade_item::fetch(array('courseid'=>$courseid, 'itemtype'=>'course'))) {
3c2e81ee 948 return $course_item;
949 }
950
951 // first get category - it creates the associated grade item
f3ac8eb4 952 $course_category = grade_category::fetch_course_category($courseid);
076aeb01 953 return $course_category->get_grade_item();
3c2e81ee 954 }
955
956 /**
957 * Is grading object editable?
958 * @return boolean
959 */
da3801e8 960 public function is_editable() {
3c2e81ee 961 return true;
962 }
963
964 /**
965 * Checks if grade calculated. Returns this object's calculation.
966 * @return boolean true if grade item calculated.
967 */
da3801e8 968 public function is_calculated() {
3c2e81ee 969 if (empty($this->calculation)) {
970 return false;
971 }
972
973 /*
974 * The main reason why we use the ##gixxx## instead of [[idnumber]] is speed of depends_on(),
975 * we would have to fetch all course grade items to find out the ids.
976 * Also if user changes the idnumber the formula does not need to be updated.
977 */
978
979 // first detect if we need to change calculation formula from [[idnumber]] to ##giXXX## (after backup, etc.)
477eec40 980 if (!$this->calculation_normalized and strpos($this->calculation, '[[') !== false) {
3c2e81ee 981 $this->set_calculation($this->calculation);
982 }
983
984 return !empty($this->calculation);
985 }
986
987 /**
988 * Returns calculation string if grade calculated.
989 * @return mixed string if calculation used, null if not
990 */
da3801e8 991 public function get_calculation() {
3c2e81ee 992 if ($this->is_calculated()) {
f3ac8eb4 993 return grade_item::denormalize_formula($this->calculation, $this->courseid);
3c2e81ee 994
995 } else {
996 return NULL;
997 }
998 }
999
1000 /**
1001 * Sets this item's calculation (creates it) if not yet set, or
1002 * updates it if already set (in the DB). If no calculation is given,
1003 * the calculation is removed.
1004 * @param string $formula string representation of formula used for calculation
1005 * @return boolean success
1006 */
da3801e8 1007 public function set_calculation($formula) {
f3ac8eb4 1008 $this->calculation = grade_item::normalize_formula($formula, $this->courseid);
3c2e81ee 1009 $this->calculation_normalized = true;
1010 return $this->update();
1011 }
1012
1013 /**
1014 * Denormalizes the calculation formula to [idnumber] form
1015 * @static
1016 * @param string $formula
1017 * @return string denormalized string
1018 */
da3801e8 1019 public static function denormalize_formula($formula, $courseid) {
3c2e81ee 1020 if (empty($formula)) {
1021 return '';
1022 }
1023
1024 // denormalize formula - convert ##giXX## to [[idnumber]]
1025 if (preg_match_all('/##gi(\d+)##/', $formula, $matches)) {
1026 foreach ($matches[1] as $id) {
f3ac8eb4 1027 if ($grade_item = grade_item::fetch(array('id'=>$id, 'courseid'=>$courseid))) {
3c2e81ee 1028 if (!empty($grade_item->idnumber)) {
1029 $formula = str_replace('##gi'.$grade_item->id.'##', '[['.$grade_item->idnumber.']]', $formula);
1030 }
1031 }
1032 }
1033 }
1034
1035 return $formula;
1036
1037 }
1038
1039 /**
1040 * Normalizes the calculation formula to [#giXX#] form
1041 * @static
1042 * @param string $formula
1043 * @return string normalized string
1044 */
da3801e8 1045 public static function normalize_formula($formula, $courseid) {
3c2e81ee 1046 $formula = trim($formula);
1047
1048 if (empty($formula)) {
1049 return NULL;
1050
1051 }
1052
1053 // normalize formula - we want grade item ids ##giXXX## instead of [[idnumber]]
f3ac8eb4 1054 if ($grade_items = grade_item::fetch_all(array('courseid'=>$courseid))) {
3c2e81ee 1055 foreach ($grade_items as $grade_item) {
1056 $formula = str_replace('[['.$grade_item->idnumber.']]', '##gi'.$grade_item->id.'##', $formula);
1057 }
1058 }
1059
1060 return $formula;
1061 }
1062
1063 /**
1064 * Returns the final values for this grade item (as imported by module or other source).
1065 * @param int $userid Optional: to retrieve a single final grade
1066 * @return mixed An array of all final_grades (stdClass objects) for this grade_item, or a single final_grade.
1067 */
da3801e8 1068 public function get_final($userid=NULL) {
1069 global $DB;
3c2e81ee 1070 if ($userid) {
da3801e8 1071 if ($user = $DB->get_record('grade_grades', array('itemid' => $this->id, 'userid' => $userid))) {
3c2e81ee 1072 return $user;
1073 }
1074
1075 } else {
da3801e8 1076 if ($grades = $DB->get_records('grade_grades', array('itemid' => $this->id))) {
3c2e81ee 1077 //TODO: speed up with better SQL
1078 $result = array();
1079 foreach ($grades as $grade) {
1080 $result[$grade->userid] = $grade;
1081 }
1082 return $result;
1083 } else {
1084 return array();
1085 }
1086 }
1087 }
1088
1089 /**
1090 * Get (or create if not exist yet) grade for this user
1091 * @param int $userid
1092 * @return object grade_grade object instance
1093 */
da3801e8 1094 public function get_grade($userid, $create=true) {
3c2e81ee 1095 if (empty($this->id)) {
1096 debugging('Can not use before insert');
1097 return false;
1098 }
1099
f3ac8eb4 1100 $grade = new grade_grade(array('userid'=>$userid, 'itemid'=>$this->id));
3c2e81ee 1101 if (empty($grade->id) and $create) {
1102 $grade->insert();
1103 }
1104
1105 return $grade;
1106 }
1107
1108 /**
1109 * Returns the sortorder of this grade_item. This method is also available in
1110 * grade_category, for cases where the object type is not know.
1111 * @return int Sort order
1112 */
da3801e8 1113 public function get_sortorder() {
3c2e81ee 1114 return $this->sortorder;
1115 }
1116
1117 /**
1118 * Returns the idnumber of this grade_item. This method is also available in
1119 * grade_category, for cases where the object type is not know.
1120 * @return string idnumber
1121 */
da3801e8 1122 public function get_idnumber() {
3c2e81ee 1123 return $this->idnumber;
1124 }
1125
1126 /**
1127 * Returns this grade_item. This method is also available in
1128 * grade_category, for cases where the object type is not know.
1129 * @return string idnumber
1130 */
da3801e8 1131 public function get_grade_item() {
3c2e81ee 1132 return $this;
1133 }
1134
1135 /**
1136 * Sets the sortorder of this grade_item. This method is also available in
1137 * grade_category, for cases where the object type is not know.
1138 * @param int $sortorder
1139 * @return void
1140 */
da3801e8 1141 public function set_sortorder($sortorder) {
25bcd908 1142 if ($this->sortorder == $sortorder) {
1143 return;
9eeb49b2 1144 }
3c2e81ee 1145 $this->sortorder = $sortorder;
1146 $this->update();
1147 }
1148
da3801e8 1149 public function move_after_sortorder($sortorder) {
5b0af8c5 1150 global $CFG, $DB;
3c2e81ee 1151
1152 //make some room first
5b0af8c5 1153 $params = array($sortorder, $this->courseid);
1154 $sql = "UPDATE {grade_items}
3c2e81ee 1155 SET sortorder = sortorder + 1
5b0af8c5 1156 WHERE sortorder > ? AND courseid = ?";
1157 $DB->execute($sql, $params);
3c2e81ee 1158
1159 $this->set_sortorder($sortorder + 1);
1160 }
1161
1162 /**
1163 * Returns the most descriptive field for this object. This is a standard method used
1164 * when we do not know the exact type of an object.
653a8648 1165 * @param boolean $fulltotal: if the item is a category total, returns $categoryname."total" instead of "Category total" or "Course total"
3c2e81ee 1166 * @return string name
1167 */
653a8648 1168 public function get_name($fulltotal=false) {
3c2e81ee 1169 if (!empty($this->itemname)) {
1170 // MDL-10557
1171 return format_string($this->itemname);
1172
1173 } else if ($this->is_course_item()) {
1174 return get_string('coursetotal', 'grades');
1175
1176 } else if ($this->is_category_item()) {
653a8648 1177 if ($fulltotal) {
121d8006 1178 $category = $this->load_parent_category();
653a8648 1179 $a = new stdClass();
1180 $a->category = $category->get_name();
1181 return get_string('categorytotalfull', 'grades', $a);
1182 } else {
3c2e81ee 1183 return get_string('categorytotal', 'grades');
653a8648 1184 }
3c2e81ee 1185
1186 } else {
1187 return get_string('grade');
1188 }
1189 }
1190
1191 /**
1192 * Sets this item's categoryid. A generic method shared by objects that have a parent id of some kind.
1193 * @param int $parentid
1194 * @return boolean success;
1195 */
da3801e8 1196 public function set_parent($parentid) {
3c2e81ee 1197 if ($this->is_course_item() or $this->is_category_item()) {
2f137aa1 1198 print_error('cannotsetparentforcatoritem');
3c2e81ee 1199 }
1200
1201 if ($this->categoryid == $parentid) {
1202 return true;
1203 }
1204
1205 // find parent and check course id
f3ac8eb4 1206 if (!$parent_category = grade_category::fetch(array('id'=>$parentid, 'courseid'=>$this->courseid))) {
3c2e81ee 1207 return false;
1208 }
1209
bb556423 1210 // MDL-19407 If moving from a non-SWM category to a SWM category, convert aggregationcoef to 0
1211 $currentparent = $this->load_parent_category();
1212
1213 if ($currentparent->aggregation != GRADE_AGGREGATE_WEIGHTED_MEAN2 && $parent_category->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN2) {
1214 $this->aggregationcoef = 0;
1215 }
1216
3c2e81ee 1217 $this->force_regrading();
1218
1219 // set new parent
1220 $this->categoryid = $parent_category->id;
1221 $this->parent_category =& $parent_category;
1222
1223 return $this->update();
1224 }
1225
653a8648 1226 /**
1227 * Makes sure value is a valid grade value.
1228 * @param float $gradevalue
1229 * @return mixed float or int fixed grade value
1230 */
1231 public function bounded_grade($gradevalue) {
1232 global $CFG;
1233
1234 if (is_null($gradevalue)) {
1235 return null;
1236 }
1237
1238 if ($this->gradetype == GRADE_TYPE_SCALE) {
1239 // no >100% grades hack for scale grades!
1240 // 1.5 is rounded to 2 ;-)
1241 return (int)bounded_number($this->grademin, round($gradevalue+0.00001), $this->grademax);
1242 }
1243
1244 $grademax = $this->grademax;
1245
1246 // NOTE: if you change this value you must manually reset the needsupdate flag in all grade items
1247 $maxcoef = isset($CFG->gradeoverhundredprocentmax) ? $CFG->gradeoverhundredprocentmax : 10; // 1000% max by default
1248
1249 if (!empty($CFG->unlimitedgrades)) {
1250 // NOTE: if you change this value you must manually reset the needsupdate flag in all grade items
1251 $grademax = $grademax * $maxcoef;
1252 } else if ($this->is_category_item() or $this->is_course_item()) {
1253 $category = $this->load_item_category();
1254 if ($category->aggregation >= 100) {
1255 // grade >100% hack
1256 $grademax = $grademax * $maxcoef;
1257 }
1258 }
1259
1260 return (float)bounded_number($this->grademin, $gradevalue, $grademax);
1261 }
1262
3c2e81ee 1263 /**
f7d515b6 1264 * Finds out on which other items does this depend directly when doing calculation or category aggregation
3c2e81ee 1265 * @param bool $reset_cache
1266 * @return array of grade_item ids this one depends on
1267 */
da3801e8 1268 public function depends_on($reset_cache=false) {
1269 global $CFG, $DB;
3c2e81ee 1270
1271 if ($reset_cache) {
1272 $this->dependson_cache = null;
1273 } else if (isset($this->dependson_cache)) {
1274 return $this->dependson_cache;
1275 }
1276
1277 if ($this->is_locked()) {
1278 // locked items do not need to be regraded
1279 $this->dependson_cache = array();
1280 return $this->dependson_cache;
1281 }
1282
1283 if ($this->is_calculated()) {
1284 if (preg_match_all('/##gi(\d+)##/', $this->calculation, $matches)) {
1285 $this->dependson_cache = array_unique($matches[1]); // remove duplicates
1286 return $this->dependson_cache;
1287 } else {
1288 $this->dependson_cache = array();
1289 return $this->dependson_cache;
1290 }
1291
1292 } else if ($grade_category = $this->load_item_category()) {
5b0af8c5 1293 $params = array();
1294
3c2e81ee 1295 //only items with numeric or scale values can be aggregated
1296 if ($this->gradetype != GRADE_TYPE_VALUE and $this->gradetype != GRADE_TYPE_SCALE) {
1297 $this->dependson_cache = array();
1298 return $this->dependson_cache;
1299 }
1300
a6771652 1301 $grade_category->apply_forced_settings();
3c2e81ee 1302
1303 if (empty($CFG->enableoutcomes) or $grade_category->aggregateoutcomes) {
1304 $outcomes_sql = "";
1305 } else {
1306 $outcomes_sql = "AND gi.outcomeid IS NULL";
1307 }
1308
91f9a62c 1309 if (empty($CFG->grade_includescalesinaggregation)) {
5b0af8c5 1310 $gtypes = "gi.gradetype = ?";
1311 $params[] = GRADE_TYPE_VALUE;
91f9a62c 1312 } else {
5b0af8c5 1313 $gtypes = "(gi.gradetype = ? OR gi.gradetype = ?)";
1314 $params[] = GRADE_TYPE_VALUE;
1315 $params[] = GRADE_TYPE_SCALE;
91f9a62c 1316 }
1317
3c2e81ee 1318 if ($grade_category->aggregatesubcats) {
1319 // return all children excluding category items
1b6ab892 1320 $params[] = '%/' . $grade_category->id . '/%';
3c2e81ee 1321 $sql = "SELECT gi.id
5b0af8c5 1322 FROM {grade_items} gi
9eeb49b2 1323 WHERE $gtypes
3c2e81ee 1324 $outcomes_sql
1325 AND gi.categoryid IN (
1326 SELECT gc.id
5b0af8c5 1327 FROM {grade_categories} gc
1b6ab892 1328 WHERE gc.path LIKE ?)";
3c2e81ee 1329 } else {
5b0af8c5 1330 $params[] = $grade_category->id;
1331 $params[] = $grade_category->id;
f5a726fe
DM
1332 if (empty($CFG->grade_includescalesinaggregation)) {
1333 $params[] = GRADE_TYPE_VALUE;
1334 } else {
1335 $params[] = GRADE_TYPE_VALUE;
1336 $params[] = GRADE_TYPE_SCALE;
1337 }
3c2e81ee 1338 $sql = "SELECT gi.id
5b0af8c5 1339 FROM {grade_items} gi
1340 WHERE $gtypes
1341 AND gi.categoryid = ?
3c2e81ee 1342 $outcomes_sql
3c2e81ee 1343 UNION
1344
1345 SELECT gi.id
5b0af8c5 1346 FROM {grade_items} gi, {grade_categories} gc
3c2e81ee 1347 WHERE (gi.itemtype = 'category' OR gi.itemtype = 'course') AND gi.iteminstance=gc.id
5b0af8c5 1348 AND gc.parent = ?
91f9a62c 1349 AND $gtypes
3c2e81ee 1350 $outcomes_sql";
1351 }
1352
5b0af8c5 1353 if ($children = $DB->get_records_sql($sql, $params)) {
3c2e81ee 1354 $this->dependson_cache = array_keys($children);
1355 return $this->dependson_cache;
1356 } else {
1357 $this->dependson_cache = array();
1358 return $this->dependson_cache;
1359 }
1360
1361 } else {
1362 $this->dependson_cache = array();
1363 return $this->dependson_cache;
1364 }
1365 }
1366
1367 /**
1994d890 1368 * Refetch grades from modules, plugins.
3c2e81ee 1369 * @param int $userid optional, one user only
1370 */
da3801e8 1371 public function refresh_grades($userid=0) {
1372 global $DB;
3c2e81ee 1373 if ($this->itemtype == 'mod') {
1374 if ($this->is_outcome_item()) {
1375 //nothing to do
1376 return;
1377 }
1378
da3801e8 1379 if (!$activity = $DB->get_record($this->itemmodule, array('id' => $this->iteminstance))) {
1994d890 1380 debugging("Can not find $this->itemmodule activity with id $this->iteminstance");
3c2e81ee 1381 return;
1382 }
1383
f3ac8eb4 1384 if (!$cm = get_coursemodule_from_instance($this->itemmodule, $activity->id, $this->courseid)) {
1994d890 1385 debugging('Can not find course module');
3c2e81ee 1386 return;
1387 }
1388
1389 $activity->modname = $this->itemmodule;
1390 $activity->cmidnumber = $cm->idnumber;
1391
1392 grade_update_mod_grades($activity);
1393 }
1394 }
1395
1396 /**
1397 * Updates final grade value for given user, this is a only way to update final
1398 * grades from gradebook and import because it logs the change in history table
1399 * and deals with overridden flag. This flag is set to prevent later overriding
1400 * from raw grades submitted from modules.
1401 *
1402 * @param int $userid the graded user
1403 * @param mixed $finalgrade float value of final grade - false means do not change
1404 * @param string $howmodified modification source
1405 * @param string $note optional note
1406 * @param mixed $feedback teachers feedback as string - false means do not change
1407 * @param int $feedbackformat
1408 * @return boolean success
1409 */
da3801e8 1410 public function update_final_grade($userid, $finalgrade=false, $source=NULL, $feedback=false, $feedbackformat=FORMAT_MOODLE, $usermodified=null) {
3c2e81ee 1411 global $USER, $CFG;
1412
3c2e81ee 1413 $result = true;
1414
1415 // no grading used or locked
1416 if ($this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) {
1417 return false;
1418 }
1419
f3ac8eb4 1420 $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid));
3c2e81ee 1421 $grade->grade_item =& $this; // prevent db fetching of this grade_item
1422
ced5ee59 1423 if (empty($usermodified)) {
1424 $grade->usermodified = $USER->id;
1425 } else {
1426 $grade->usermodified = $usermodified;
1427 }
3c2e81ee 1428
1429 if ($grade->is_locked()) {
1430 // do not update locked grades at all
1431 return false;
1432 }
1433
1434 $locktime = $grade->get_locktime();
1435 if ($locktime and $locktime < time()) {
1436 // do not update grades that should be already locked, force regrade instead
1437 $this->force_regrading();
1438 return false;
1439 }
1440
365a5941 1441 $oldgrade = new stdClass();
3c2e81ee 1442 $oldgrade->finalgrade = $grade->finalgrade;
1443 $oldgrade->overridden = $grade->overridden;
1444 $oldgrade->feedback = $grade->feedback;
1445 $oldgrade->feedbackformat = $grade->feedbackformat;
1446
0f392ff4 1447 // changed grade?
1448 if ($finalgrade !== false) {
1449 if ($this->is_overridable_item()) {
3c2e81ee 1450 $grade->overridden = time();
1451 }
3c2e81ee 1452
653a8648 1453 $grade->finalgrade = $this->bounded_grade($finalgrade);
3c2e81ee 1454 }
1455
1456 // do we have comment from teacher?
1457 if ($feedback !== false) {
5048575d 1458 if ($this->is_overridable_item_feedback()) {
0f392ff4 1459 // external items (modules, plugins) may have own feedback
1460 $grade->overridden = time();
1461 }
1462
3c2e81ee 1463 $grade->feedback = $feedback;
1464 $grade->feedbackformat = $feedbackformat;
1465 }
1466
1467 if (empty($grade->id)) {
9eeb49b2 1468 $grade->timecreated = null; // hack alert - date submitted - no submission yet
1469 $grade->timemodified = time(); // hack alert - date graded
3c2e81ee 1470 $result = (boolean)$grade->insert($source);
1471
25bcd908 1472 } else if (grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)
1473 or $grade->feedback !== $oldgrade->feedback
5048575d 1474 or $grade->feedbackformat != $oldgrade->feedbackformat
26e3ae92 1475 or ($oldgrade->overridden == 0 and $grade->overridden > 0)) {
9eeb49b2 1476 $grade->timemodified = time(); // hack alert - date graded
3c2e81ee 1477 $result = $grade->update($source);
0f392ff4 1478 } else {
1479 // no grade change
1480 return $result;
3c2e81ee 1481 }
1482
1483 if (!$result) {
1484 // something went wrong - better force final grade recalculation
1485 $this->force_regrading();
1486
1487 } else if ($this->is_course_item() and !$this->needsupdate) {
b45d8391 1488 if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
3c2e81ee 1489 $this->force_regrading();
1490 }
1491
1492 } else if (!$this->needsupdate) {
f3ac8eb4 1493 $course_item = grade_item::fetch_course_item($this->courseid);
3c2e81ee 1494 if (!$course_item->needsupdate) {
b45d8391 1495 if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
3c2e81ee 1496 $this->force_regrading();
1497 }
1498 } else {
1499 $this->force_regrading();
1500 }
1501 }
1502
1503 return $result;
1504 }
1505
1506
1507 /**
1508 * Updates raw grade value for given user, this is a only way to update raw
1509 * grades from external source (modules, etc.),
1510 * because it logs the change in history table and deals with final grade recalculation.
1511 *
1512 * @param int $userid the graded user
1513 * @param mixed $rawgrade float value of raw grade - false means do not change
1514 * @param string $howmodified modification source
1515 * @param string $note optional note
1516 * @param mixed $feedback teachers feedback as string - false means do not change
1517 * @param int $feedbackformat
ced5ee59 1518 * @param int $usermodified - user which did the grading
1519 * @param int $dategraded
1520 * @param int $datesubmitted
f7d515b6 1521 * @param object $grade object - useful for bulk upgrades
3c2e81ee 1522 * @return boolean success
1523 */
da3801e8 1524 public function update_raw_grade($userid, $rawgrade=false, $source=NULL, $feedback=false, $feedbackformat=FORMAT_MOODLE, $usermodified=null, $dategraded=null, $datesubmitted=null, $grade=null) {
3c2e81ee 1525 global $USER;
1526
3c2e81ee 1527 $result = true;
1528
1529 // calculated grades can not be updated; course and category can not be updated because they are aggregated
0f392ff4 1530 if (!$this->is_raw_used() or $this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) {
3c2e81ee 1531 return false;
1532 }
1533
55231be0 1534 if (is_null($grade)) {
1535 //fetch from db
1536 $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid));
1537 }
3c2e81ee 1538 $grade->grade_item =& $this; // prevent db fetching of this grade_item
1539
ced5ee59 1540 if (empty($usermodified)) {
1541 $grade->usermodified = $USER->id;
1542 } else {
1543 $grade->usermodified = $usermodified;
1544 }
1545
3c2e81ee 1546 if ($grade->is_locked()) {
1547 // do not update locked grades at all
1548 return false;
1549 }
1550
1551 $locktime = $grade->get_locktime();
1552 if ($locktime and $locktime < time()) {
1553 // do not update grades that should be already locked and force regrade
1554 $this->force_regrading();
1555 return false;
1556 }
1557
365a5941 1558 $oldgrade = new stdClass();
66690b69 1559 $oldgrade->finalgrade = $grade->finalgrade;
1560 $oldgrade->rawgrade = $grade->rawgrade;
1561 $oldgrade->rawgrademin = $grade->rawgrademin;
1562 $oldgrade->rawgrademax = $grade->rawgrademax;
1563 $oldgrade->rawscaleid = $grade->rawscaleid;
3c2e81ee 1564 $oldgrade->feedback = $grade->feedback;
1565 $oldgrade->feedbackformat = $grade->feedbackformat;
1566
b45d8391 1567 // use new min and max
66690b69 1568 $grade->rawgrade = $grade->rawgrade;
1569 $grade->rawgrademin = $this->grademin;
1570 $grade->rawgrademax = $this->grademax;
1571 $grade->rawscaleid = $this->scaleid;
3c2e81ee 1572
1573 // change raw grade?
1574 if ($rawgrade !== false) {
66690b69 1575 $grade->rawgrade = $rawgrade;
3c2e81ee 1576 }
1577
9eeb49b2 1578 // empty feedback means no feedback at all
1579 if ($feedback === '') {
1580 $feedback = null;
1581 }
1582
3c2e81ee 1583 // do we have comment from teacher?
25bcd908 1584 if ($feedback !== false and !$grade->is_overridden()) {
3c2e81ee 1585 $grade->feedback = $feedback;
1586 $grade->feedbackformat = $feedbackformat;
1587 }
1588
b45d8391 1589 // update final grade if possible
1590 if (!$grade->is_locked() and !$grade->is_overridden()) {
66690b69 1591 $grade->finalgrade = $this->adjust_raw_grade($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
b45d8391 1592 }
1593
9eeb49b2 1594 // TODO: hack alert - create new fields for these in 2.0
1595 $oldgrade->timecreated = $grade->timecreated;
1596 $oldgrade->timemodified = $grade->timemodified;
1597
1598 $grade->timecreated = $datesubmitted;
1599
1600 if ($grade->is_overridden()) {
1601 // keep original graded date - update_final_grade() sets this for overridden grades
1602
1603 } else if (is_null($grade->rawgrade) and is_null($grade->feedback)) {
1604 // no grade and feedback means no grading yet
1605 $grade->timemodified = null;
1606
1607 } else if (!empty($dategraded)) {
1608 // fine - module sends info when graded (yay!)
1609 $grade->timemodified = $dategraded;
1610
1611 } else if (grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)
1612 or $grade->feedback !== $oldgrade->feedback) {
1613 // guess - if either grade or feedback changed set new graded date
1614 $grade->timemodified = time();
1615
1616 } else {
1617 //keep original graded date
717f432f 1618 }
9eeb49b2 1619 // end of hack alert
717f432f 1620
3c2e81ee 1621 if (empty($grade->id)) {
1622 $result = (boolean)$grade->insert($source);
1623
25bcd908 1624 } else if (grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)
1625 or grade_floats_different($grade->rawgrade, $oldgrade->rawgrade)
1626 or grade_floats_different($grade->rawgrademin, $oldgrade->rawgrademin)
1627 or grade_floats_different($grade->rawgrademax, $oldgrade->rawgrademax)
1628 or $grade->rawscaleid != $oldgrade->rawscaleid
1629 or $grade->feedback !== $oldgrade->feedback
9eeb49b2 1630 or $grade->feedbackformat != $oldgrade->feedbackformat
1631 or $grade->timecreated != $oldgrade->timecreated // part of hack above
1632 or $grade->timemodified != $oldgrade->timemodified // part of hack above
1633 ) {
3c2e81ee 1634 $result = $grade->update($source);
66690b69 1635 } else {
1636 return $result;
3c2e81ee 1637 }
1638
1639 if (!$result) {
1640 // something went wrong - better force final grade recalculation
1641 $this->force_regrading();
1642
1643 } else if (!$this->needsupdate) {
f3ac8eb4 1644 $course_item = grade_item::fetch_course_item($this->courseid);
3c2e81ee 1645 if (!$course_item->needsupdate) {
b45d8391 1646 if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
3c2e81ee 1647 $this->force_regrading();
1648 }
3c2e81ee 1649 }
1650 }
1651
1652 return $result;
1653 }
1654
1655 /**
1656 * Calculates final grade values using the formula in calculation property.
1657 * The parameters are taken from final grades of grade items in current course only.
1658 * @return boolean false if error
1659 */
da3801e8 1660 public function compute($userid=null) {
1661 global $CFG, $DB;
3c2e81ee 1662
1663 if (!$this->is_calculated()) {
1664 return false;
1665 }
1666
1667 require_once($CFG->libdir.'/mathslib.php');
1668
1669 if ($this->is_locked()) {
1670 return true; // no need to recalculate locked items
1671 }
1672
52e95845 1673 // precreate grades - we need them to exist
f53db007 1674 $params = array($this->courseid, $this->id, $this->id);
52e95845 1675 $sql = "SELECT DISTINCT go.userid
5b0af8c5 1676 FROM {grade_grades} go
1677 JOIN {grade_items} gi
f53db007 1678 ON (gi.id = go.itemid AND gi.courseid = ?)
5b0af8c5 1679 LEFT OUTER JOIN {grade_grades} g
1680 ON (g.userid = go.userid AND g.itemid = ?)
f53db007 1681 WHERE gi.id <> ? AND g.id IS NULL";
5b0af8c5 1682 if ($missing = $DB->get_records_sql($sql, $params)) {
52e95845 1683 foreach ($missing as $m) {
1684 $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$m->userid), false);
1685 $grade->grade_item =& $this;
1686 $grade->insert('system');
1687 }
1688 }
1689
3c2e81ee 1690 // get used items
1691 $useditems = $this->depends_on();
1692
1693 // prepare formula and init maths library
1694 $formula = preg_replace('/##(gi\d+)##/', '\1', $this->calculation);
9c9a3259 1695 if (strpos($formula, '[[') !== false) {
1696 // missing item
1697 return false;
1698 }
3c2e81ee 1699 $this->formula = new calc_formula($formula);
1700
1701 // where to look for final grades?
1702 // this itemid is added so that we use only one query for source and final grades
5b0af8c5 1703 $gis = array_merge($useditems, array($this->id));
1704 list($usql, $params) = $DB->get_in_or_equal($gis);
3c2e81ee 1705
1706 if ($userid) {
5b0af8c5 1707 $usersql = "AND g.userid=?";
1708 $params[] = $userid;
3c2e81ee 1709 } else {
1710 $usersql = "";
1711 }
1712
f3ac8eb4 1713 $grade_inst = new grade_grade();
3c2e81ee 1714 $fields = 'g.'.implode(',g.', $grade_inst->required_fields);
1715
5b0af8c5 1716 $params[] = $this->courseid;
3c2e81ee 1717 $sql = "SELECT $fields
5b0af8c5 1718 FROM {grade_grades} g, {grade_items} gi
1719 WHERE gi.id = g.itemid AND gi.id $usql $usersql AND gi.courseid=?
1720 ORDER BY g.userid";
3c2e81ee 1721
1722 $return = true;
1723
1724 // group the grades by userid and use formula on the group
1b42e677
EL
1725 $rs = $DB->get_recordset_sql($sql, $params);
1726 if ($rs->valid()) {
3c2e81ee 1727 $prevuser = 0;
1728 $grade_records = array();
1729 $oldgrade = null;
da3801e8 1730 foreach ($rs as $used) {
3c2e81ee 1731 if ($used->userid != $prevuser) {
1732 if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {
1733 $return = false;
1734 }
1735 $prevuser = $used->userid;
1736 $grade_records = array();
1737 $oldgrade = null;
1738 }
1739 if ($used->itemid == $this->id) {
1740 $oldgrade = $used;
1741 }
1742 $grade_records['gi'.$used->itemid] = $used->finalgrade;
1743 }
1744 if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {
1745 $return = false;
1746 }
1747 }
1b42e677 1748 $rs->close();
3c2e81ee 1749
1750 return $return;
1751 }
1752
1753 /**
1754 * internal function - does the final grade calculation
1755 */
da3801e8 1756 public function use_formula($userid, $params, $useditems, $oldgrade) {
3c2e81ee 1757 if (empty($userid)) {
1758 return true;
1759 }
1760
1761 // add missing final grade values
1762 // not graded (null) is counted as 0 - the spreadsheet way
e40eabb5 1763 $allinputsnull = true;
3c2e81ee 1764 foreach($useditems as $gi) {
e40eabb5 1765 if (!array_key_exists('gi'.$gi, $params) || is_null($params['gi'.$gi])) {
3c2e81ee 1766 $params['gi'.$gi] = 0;
1767 } else {
1768 $params['gi'.$gi] = (float)$params['gi'.$gi];
e40eabb5
AD
1769 if ($gi != $this->id) {
1770 $allinputsnull = false;
1771 }
3c2e81ee 1772 }
1773 }
1774
1775 // can not use own final grade during calculation
1776 unset($params['gi'.$this->id]);
1777
1778 // insert final grade - will be needed later anyway
1779 if ($oldgrade) {
66690b69 1780 $oldfinalgrade = $oldgrade->finalgrade;
f3ac8eb4 1781 $grade = new grade_grade($oldgrade, false); // fetching from db is not needed
3c2e81ee 1782 $grade->grade_item =& $this;
1783
1784 } else {
f3ac8eb4 1785 $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid), false);
3c2e81ee 1786 $grade->grade_item =& $this;
4ac209d5 1787 $grade->insert('system');
1788 $oldfinalgrade = null;
3c2e81ee 1789 }
1790
1791 // no need to recalculate locked or overridden grades
1792 if ($grade->is_locked() or $grade->is_overridden()) {
1793 return true;
1794 }
1795
e40eabb5 1796 if ($allinputsnull) {
3c2e81ee 1797 $grade->finalgrade = null;
e40eabb5 1798 $result = true;
3c2e81ee 1799
1800 } else {
e40eabb5
AD
1801
1802 // do the calculation
1803 $this->formula->set_params($params);
1804 $result = $this->formula->evaluate();
1805
1806 if ($result === false) {
1807 $grade->finalgrade = null;
1808
1809 } else {
1810 // normalize
1811 $grade->finalgrade = $this->bounded_grade($result);
1812 }
1813
3c2e81ee 1814 }
1815
1816 // update in db if changed
25bcd908 1817 if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) {
e785b784 1818 $grade->timemodified = time();
4ac209d5 1819 $grade->update('compute');
3c2e81ee 1820 }
1821
1822 if ($result !== false) {
1823 //lock grade if needed
1824 }
1825
1826 if ($result === false) {
1827 return false;
1828 } else {
1829 return true;
1830 }
1831
1832 }
1833
1834 /**
1835 * Validate the formula.
1836 * @param string $formula
1837 * @return boolean true if calculation possible, false otherwise
1838 */
da3801e8 1839 public function validate_formula($formulastr) {
1840 global $CFG, $DB;
3c2e81ee 1841 require_once($CFG->libdir.'/mathslib.php');
1842
f3ac8eb4 1843 $formulastr = grade_item::normalize_formula($formulastr, $this->courseid);
3c2e81ee 1844
1845 if (empty($formulastr)) {
1846 return true;
1847 }
1848
1849 if (strpos($formulastr, '=') !== 0) {
1850 return get_string('errorcalculationnoequal', 'grades');
1851 }
1852
1853 // get used items
1854 if (preg_match_all('/##gi(\d+)##/', $formulastr, $matches)) {
1855 $useditems = array_unique($matches[1]); // remove duplicates
1856 } else {
1857 $useditems = array();
1858 }
ced5ee59 1859
26d7de8b 1860 // MDL-11902
1861 // unset the value if formula is trying to reference to itself
1862 // but array keys does not match itemid
3c2e81ee 1863 if (!empty($this->id)) {
26d7de8b 1864 $useditems = array_diff($useditems, array($this->id));
1865 //unset($useditems[$this->id]);
3c2e81ee 1866 }
1867
1868 // prepare formula and init maths library
1869 $formula = preg_replace('/##(gi\d+)##/', '\1', $formulastr);
1870 $formula = new calc_formula($formula);
1871
1872
1873 if (empty($useditems)) {
1874 $grade_items = array();
1875
1876 } else {
5b0af8c5 1877 list($usql, $params) = $DB->get_in_or_equal($useditems);
1878 $params[] = $this->courseid;
3c2e81ee 1879 $sql = "SELECT gi.*
5b0af8c5 1880 FROM {grade_items} gi
1881 WHERE gi.id $usql and gi.courseid=?"; // from the same course only!
3c2e81ee 1882
5b0af8c5 1883 if (!$grade_items = $DB->get_records_sql($sql, $params)) {
3c2e81ee 1884 $grade_items = array();
1885 }
1886 }
1887
1888 $params = array();
1889 foreach ($useditems as $itemid) {
1890 // make sure all grade items exist in this course
1891 if (!array_key_exists($itemid, $grade_items)) {
1892 return false;
1893 }
1894 // use max grade when testing formula, this should be ok in 99.9%
1895 // division by 0 is one of possible problems
1896 $params['gi'.$grade_items[$itemid]->id] = $grade_items[$itemid]->grademax;
1897 }
1898
1899 // do the calculation
1900 $formula->set_params($params);
1901 $result = $formula->evaluate();
1902
1903 // false as result indicates some problem
1904 if ($result === false) {
1905 // TODO: add more error hints
1906 return get_string('errorcalculationunknown', 'grades');
1907 } else {
1908 return true;
1909 }
1910 }
1911
1912 /**
1913 * Returns the value of the display type. It can be set at 3 levels: grade_item, course setting and site. The lowest level overrides the higher ones.
1914 * @return int Display type
1915 */
da3801e8 1916 public function get_displaytype() {
3c2e81ee 1917 global $CFG;
1918
1919 if ($this->display == GRADE_DISPLAY_TYPE_DEFAULT) {
1920 return grade_get_setting($this->courseid, 'displaytype', $CFG->grade_displaytype);
1921
1922 } else {
1923 return $this->display;
1924 }
1925 }
1926
1927 /**
1928 * Returns the value of the decimals field. It can be set at 3 levels: grade_item, course setting and site. The lowest level overrides the higher ones.
1929 * @return int Decimals (0 - 5)
1930 */
da3801e8 1931 public function get_decimals() {
3c2e81ee 1932 global $CFG;
1933
1934 if (is_null($this->decimals)) {
1935 return grade_get_setting($this->courseid, 'decimalpoints', $CFG->grade_decimalpoints);
1936
1937 } else {
1938 return $this->decimals;
1939 }
1940 }
4dc81cc7 1941
9fb16349 1942 /**
1943 * Returns a string representing the range of grademin - grademax for this grade item.
1944 * @param int $rangesdisplaytype
1945 * @param int $rangesdecimalpoints
1946 * @return string
1947 */
4dc81cc7 1948 function get_formatted_range($rangesdisplaytype=null, $rangesdecimalpoints=null) {
1949
1950 global $USER;
1951
1952 // Determine which display type to use for this average
59ee3144 1953 if (isset($USER->gradeediting) && array_key_exists($this->courseid, $USER->gradeediting) && $USER->gradeediting[$this->courseid]) {
4dc81cc7 1954 $displaytype = GRADE_DISPLAY_TYPE_REAL;
1955
1956 } else if ($rangesdisplaytype == GRADE_REPORT_PREFERENCE_INHERIT) { // no ==0 here, please resave report and user prefs
1957 $displaytype = $this->get_displaytype();
1958
1959 } else {
1960 $displaytype = $rangesdisplaytype;
1961 }
1962
1963 // Override grade_item setting if a display preference (not default) was set for the averages
1964 if ($rangesdecimalpoints == GRADE_REPORT_PREFERENCE_INHERIT) {
1965 $decimalpoints = $this->get_decimals();
1966
1967 } else {
1968 $decimalpoints = $rangesdecimalpoints;
1969 }
1970
1971 if ($displaytype == GRADE_DISPLAY_TYPE_PERCENTAGE) {
1972 $grademin = "0 %";
1973 $grademax = "100 %";
1974
1975 } else {
1976 $grademin = grade_format_gradevalue($this->grademin, $this, true, $displaytype, $decimalpoints);
1977 $grademax = grade_format_gradevalue($this->grademax, $this, true, $displaytype, $decimalpoints);
1978 }
1979
1980 return $grademin.'&ndash;'. $grademax;
1981 }
653a8648 1982
1983 /**
1984 * Queries parent categories recursively to find the aggregationcoef type that applies to this
1985 * grade item.
1986 */
1987 public function get_coefstring() {
121d8006 1988 $parent_category = $this->load_parent_category();
653a8648 1989 if ($this->is_category_item()) {
121d8006 1990 $parent_category = $parent_category->load_parent_category();
653a8648 1991 }
1992
1993 if ($parent_category->is_aggregationcoef_used()) {
1994 return $parent_category->get_coefstring();
1995 } else {
1996 return false;
1997 }
1998 }
3c2e81ee 1999}