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