MDL-12182 More grade_item unit tests
[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.
248 * Force regrading if necessary
249 * @param string $source from where was the object inserted (mod/forum, manual, etc.)
250 * @return boolean success
251 */
252 function update($source=null) {
253 // reset caches
254 $this->dependson_cache = null;
255
256 // Retrieve scale and infer grademax/min from it if needed
257 $this->load_scale();
258
259 // make sure there is not 0 in outcomeid
260 if (empty($this->outcomeid)) {
261 $this->outcomeid = null;
262 }
263
264 if ($this->qualifies_for_regrading()) {
265 $this->force_regrading();
266 }
267
ced5ee59 268 $this->timemodified = time();
269
3c2e81ee 270 return parent::update($source);
271 }
272
273 /**
274 * Compares the values held by this object with those of the matching record in DB, and returns
275 * whether or not these differences are sufficient to justify an update of all parent objects.
276 * This assumes that this object has an id number and a matching record in DB. If not, it will return false.
277 * @return boolean
278 */
279 function qualifies_for_regrading() {
280 if (empty($this->id)) {
281 return false;
282 }
283
4fc9ec1e 284 $db_item = $this->get_instance('grade_item', array('id' => $this->id));
3c2e81ee 285
dea2f0d9 286 $calculationdiff = $db_item->calculation != $this->calculation;
287 $categorydiff = $db_item->categoryid != $this->categoryid;
288 $gradetypediff = $db_item->gradetype != $this->gradetype;
289 $grademaxdiff = $db_item->grademax != $this->grademax;
290 $grademindiff = $db_item->grademin != $this->grademin;
291 $scaleiddiff = $db_item->scaleid != $this->scaleid;
292 $outcomeiddiff = $db_item->outcomeid != $this->outcomeid;
293 $multfactordiff = $db_item->multfactor != $this->multfactor;
294 $plusfactordiff = $db_item->plusfactor != $this->plusfactor;
295 $locktimediff = $db_item->locktime != $this->locktime;
3c2e81ee 296 $acoefdiff = $db_item->aggregationcoef != $this->aggregationcoef;
297
298 $needsupdatediff = !$db_item->needsupdate && $this->needsupdate; // force regrading only if setting the flag first time
299 $lockeddiff = !empty($db_item->locked) && empty($this->locked); // force regrading only when unlocking
300
301 return ($calculationdiff || $categorydiff || $gradetypediff || $grademaxdiff || $grademindiff || $scaleiddiff
302 || $outcomeiddiff || $multfactordiff || $plusfactordiff || $needsupdatediff
303 || $lockeddiff || $acoefdiff || $locktimediff);
304 }
305
306 /**
307 * Finds and returns a grade_item instance based on params.
308 * @static
309 *
310 * @param array $params associative arrays varname=>value
311 * @return object grade_item instance or false if none found.
312 */
313 function fetch($params) {
795bee34 314 $obj = grade_object::get_instance('grade_item');
315 return $obj->fetch_helper('grade_items', 'grade_item', $params);
3c2e81ee 316 }
317
318 /**
319 * Finds and returns all grade_item instances based on params.
320 * @static
321 *
322 * @param array $params associative arrays varname=>value
323 * @return array array of grade_item insatnces or false if none found.
324 */
325 function fetch_all($params) {
795bee34 326 $obj = grade_object::get_instance('grade_item');
327 return $obj->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 */
335 function delete($source=null) {
336 if (!$this->is_course_item()) {
337 $this->force_regrading();
338 }
4ac209d5 339
aaefeda4 340 $grade_grade = grade_object::get_instance('grade_grade');
4fc9ec1e 341 if ($grades = $grade_grade->fetch_all(array('itemid'=>$this->id))) {
3c2e81ee 342 foreach ($grades as $grade) {
343 $grade->delete($source);
344 }
345 }
346
347 return parent::delete($source);
348 }
349
350 /**
351 * In addition to perform parent::insert(), calls force_regrading() method too.
352 * @param string $source from where was the object inserted (mod/forum, manual, etc.)
353 * @return int PK ID if successful, false otherwise
354 */
355 function insert($source=null) {
356 global $CFG;
357
358 if (empty($this->courseid)) {
359 error('Can not insert grade item without course id!');
360 }
361
362 // load scale if needed
363 $this->load_scale();
364
365 // add parent category if needed
aaefeda4 366 $grade_category = grade_object::get_instance('grade_category');
3c2e81ee 367 if (empty($this->categoryid) and !$this->is_course_item() and !$this->is_category_item()) {
4fc9ec1e 368 $course_category = $grade_category->fetch_course_category($this->courseid);
3c2e81ee 369 $this->categoryid = $course_category->id;
370
371 }
372
373 // always place the new items at the end, move them after insert if needed
4fc9ec1e 374 $last_sortorder = $this->lib_wrapper->get_field_select('grade_items', 'MAX(sortorder)', "courseid = {$this->courseid}");
3c2e81ee 375 if (!empty($last_sortorder)) {
376 $this->sortorder = $last_sortorder + 1;
377 } else {
378 $this->sortorder = 1;
379 }
380
381 // add proper item numbers to manual items
382 if ($this->itemtype == 'manual') {
383 if (empty($this->itemnumber)) {
384 $this->itemnumber = 0;
385 }
386 }
387
388 // make sure there is not 0 in outcomeid
389 if (empty($this->outcomeid)) {
390 $this->outcomeid = null;
391 }
392
ced5ee59 393 $this->timecreated = $this->timemodified = time();
394
3c2e81ee 395 if (parent::insert($source)) {
396 // force regrading of items if needed
397 $this->force_regrading();
398 return $this->id;
399
400 } else {
401 debugging("Could not insert this grade_item in the database!");
402 return false;
403 }
404 }
405
406 /**
407 * Set idnumber of grade item, updates also course_modules table
408 * @param string $idnumber (without magic quotes)
409 * @return boolean success
410 */
411 function add_idnumber($idnumber) {
412 if (!empty($this->idnumber)) {
413 return false;
414 }
415
416 if ($this->itemtype == 'mod' and !$this->is_outcome_item()) {
4fc9ec1e 417 if (!$cm = $this->lib_wrapper->get_coursemodule_from_instance($this->itemmodule, $this->iteminstance, $this->courseid)) {
3c2e81ee 418 return false;
419 }
420 if (!empty($cm->idnumber)) {
421 return false;
422 }
4fc9ec1e 423 if ($this->lib_wrapper->set_field('course_modules', 'idnumber', addslashes($idnumber), 'id', $cm->id)) {
3c2e81ee 424 $this->idnumber = $idnumber;
425 return $this->update();
426 }
427 return false;
428
429 } else {
430 $this->idnumber = $idnumber;
431 return $this->update();
432 }
433 }
434
435 /**
436 * Returns the locked state of this grade_item (if the grade_item is locked OR no specific
437 * $userid is given) or the locked state of a specific grade within this item if a specific
438 * $userid is given and the grade_item is unlocked.
439 *
440 * @param int $userid
441 * @return boolean Locked state
442 */
443 function is_locked($userid=NULL) {
444 if (!empty($this->locked)) {
445 return true;
446 }
447
448 if (!empty($userid)) {
aaefeda4 449 $grade_grade = grade_object::get_instance('grade_grade');
4fc9ec1e 450 if ($grade = $grade_grade->fetch(array('itemid'=>$this->id, 'userid'=>$userid))) {
3c2e81ee 451 $grade->grade_item =& $this; // prevent db fetching of cached grade_item
452 return $grade->is_locked();
453 }
454 }
455
456 return false;
457 }
458
459 /**
460 * Locks or unlocks this grade_item and (optionally) all its associated final grades.
461 * @param int $locked 0, 1 or a timestamp int(10) after which date the item will be locked.
462 * @param boolean $cascade lock/unlock child objects too
463 * @param boolean $refresh refresh grades when unlocking
464 * @return boolean true if grade_item all grades updated, false if at least one update fails
465 */
466 function set_locked($lockedstate, $cascade=false, $refresh=true) {
467 if ($lockedstate) {
468 /// setting lock
469 if ($this->needsupdate) {
470 return false; // can not lock grade without first having final grade
471 }
472
473 $this->locked = time();
474 $this->update();
475
476 if ($cascade) {
477 $grades = $this->get_final();
478 foreach($grades as $g) {
4fc9ec1e 479 $grade = $this->get_instance('grade_grade', $g, false);
3c2e81ee 480 $grade->grade_item =& $this;
481 $grade->set_locked(1, null, false);
482 }
483 }
484
485 return true;
486
487 } else {
488 /// removing lock
489 if (!empty($this->locked) and $this->locktime < time()) {
490 //we have to reset locktime or else it would lock up again
491 $this->locktime = 0;
492 }
493
494 $this->locked = 0;
495 $this->update();
496
497 if ($cascade) {
aaefeda4 498 $grade_grade = grade_object::get_instance('grade_grade');
4fc9ec1e 499 if ($grades = $grade_grade->fetch_all(array('itemid'=>$this->id))) {
3c2e81ee 500 foreach($grades as $grade) {
501 $grade->grade_item =& $this;
502 $grade->set_locked(0, null, false);
503 }
504 }
505 }
506
507 if ($refresh) {
508 //refresh when unlocking
509 $this->refresh_grades();
510 }
511
512 return true;
513 }
514 }
515
516 /**
517 * Lock the grade if needed - make sure this is called only when final grades are valid
518 */
519 function check_locktime() {
520 if (!empty($this->locked)) {
521 return; // already locked
522 }
523
524 if ($this->locktime and $this->locktime < time()) {
525 $this->locked = time();
526 $this->update('locktime');
527 }
528 }
529
530 /**
531 * Set the locktime for this grade item.
532 *
533 * @param int $locktime timestamp for lock to activate
534 * @return void
535 */
536 function set_locktime($locktime) {
537 $this->locktime = $locktime;
538 $this->update();
539 }
540
541 /**
542 * Set the locktime for this grade item.
543 *
544 * @return int $locktime timestamp for lock to activate
545 */
546 function get_locktime() {
547 return $this->locktime;
548 }
549
550 /**
551 * Returns the hidden state of this grade_item
552 * @return boolean hidden state
553 */
554 function is_hidden() {
555 return ($this->hidden == 1 or ($this->hidden != 0 and $this->hidden > time()));
556 }
557
597f50e6 558 /**
559 * Check grade hidden status. Uses data from both grade item and grade.
560 * @return boolean true if hiddenuntil, false if not
561 */
562 function is_hiddenuntil() {
563 return $this->hidden > 1;
564 }
565
3c2e81ee 566 /**
567 * Check grade item hidden status.
568 * @return int 0 means visible, 1 hidden always, timestamp hidden until
569 */
570 function get_hidden() {
571 return $this->hidden;
572 }
573
574 /**
575 * Set the hidden status of grade_item and all grades, 0 mean visible, 1 always hidden, number means date to hide until.
576 * @param int $hidden new hidden status
577 * @param boolean $cascade apply to child objects too
578 * @return void
579 */
580 function set_hidden($hidden, $cascade=false) {
581 $this->hidden = $hidden;
582 $this->update();
583
584 if ($cascade) {
aaefeda4 585 $grade_grade = grade_object::get_instance('grade_grade');
4fc9ec1e 586 if ($grades = $grade_grade->fetch_all(array('itemid'=>$this->id))) {
3c2e81ee 587 foreach($grades as $grade) {
588 $grade->grade_item =& $this;
589 $grade->set_hidden($hidden, $cascade);
590 }
591 }
592 }
593 }
594
595 /**
596 * Returns the number of grades that are hidden.
597 * @param return int Number of hidden grades
598 */
599 function has_hidden_grades($groupsql="", $groupwheresql="") {
600 global $CFG;
4fc9ec1e 601 return $this->lib_wrapper->get_field_sql("SELECT COUNT(*) FROM {$CFG->prefix}grade_grades g LEFT JOIN "
3c2e81ee 602 ."{$CFG->prefix}user u ON g.userid = u.id $groupsql WHERE itemid = $this->id AND hidden = 1 $groupwheresql");
603 }
604
605 /**
606 * Mark regrading as finished successfully.
607 */
608 function regrading_finished() {
609 $this->needsupdate = 0;
610 //do not use $this->update() because we do not want this logged in grade_item_history
4fc9ec1e 611 $this->lib_wrapper->set_field('grade_items', 'needsupdate', 0, 'id', $this->id);
3c2e81ee 612 }
613
614 /**
615 * Performs the necessary calculations on the grades_final referenced by this grade_item.
616 * Also resets the needsupdate flag once successfully performed.
617 *
618 * This function must be used ONLY from lib/gradeslib.php/grade_regrade_final_grades(),
619 * because the regrading must be done in correct order!!
620 *
621 * @return boolean true if ok, error string otherwise
622 */
623 function regrade_final_grades($userid=null) {
624 global $CFG;
625
626 // locked grade items already have correct final grades
627 if ($this->is_locked()) {
628 return true;
629 }
630
631 // calculation produces final value using formula from other final values
632 if ($this->is_calculated()) {
633 if ($this->compute($userid)) {
634 return true;
635 } else {
636 return "Could not calculate grades for grade item"; // TODO: improve and localize
637 }
638
639 // noncalculated outcomes already have final values - raw grades not used
640 } else if ($this->is_outcome_item()) {
641 return true;
642
643 // aggregate the category grade
644 } else if ($this->is_category_item() or $this->is_course_item()) {
645 // aggregate category grade item
646 $category = $this->get_item_category();
647 $category->grade_item =& $this;
648 if ($category->generate_grades($userid)) {
649 return true;
650 } else {
651 return "Could not aggregate final grades for category:".$this->id; // TODO: improve and localize
652 }
653
654 } else if ($this->is_manual_item()) {
655 // manual items track only final grades, no raw grades
656 return true;
657
658 } else if (!$this->is_raw_used()) {
659 // hmm - raw grades are not used- nothing to regrade
660 return true;
661 }
662
663 // normal grade item - just new final grades
664 $result = true;
aaefeda4 665 $grade_inst = grade_object::get_instance('grade_grade');
3c2e81ee 666 $fields = implode(',', $grade_inst->required_fields);
667 if ($userid) {
4fc9ec1e 668 $rs = $this->lib_wrapper->get_recordset_select('grade_grades', "itemid={$this->id} AND userid=$userid", '', $fields);
3c2e81ee 669 } else {
4fc9ec1e 670 $rs = $this->lib_wrapper->get_recordset('grade_grades', 'itemid', $this->id, '', $fields);
3c2e81ee 671 }
672 if ($rs) {
4fc9ec1e 673 while ($grade_record = $this->lib_wrapper->rs_fetch_next_record($rs)) {
674 $grade = $this->get_instance('grade_grade', $grade_record, false);
3c2e81ee 675
676 if (!empty($grade_record->locked) or !empty($grade_record->overridden)) {
677 // this grade is locked - final grade must be ok
678 continue;
679 }
680
681 $grade->finalgrade = $this->adjust_raw_grade($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
682
683 if ($grade_record->finalgrade !== $grade->finalgrade) {
684 if (!$grade->update('system')) {
685 $result = "Internal error updating final grade";
686 }
687 }
688 }
4fc9ec1e 689 $this->lib_wrapper->rs_close($rs);
3c2e81ee 690 }
691
692 return $result;
693 }
694
695 /**
696 * Given a float grade value or integer grade scale, applies a number of adjustment based on
697 * grade_item variables and returns the result.
698 * @param object $rawgrade The raw grade value.
699 * @return mixed
700 */
701 function adjust_raw_grade($rawgrade, $rawmin, $rawmax) {
702 if (is_null($rawgrade)) {
703 return null;
704 }
705
706 if ($this->gradetype == GRADE_TYPE_VALUE) { // Dealing with numerical grade
707
708 if ($this->grademax < $this->grademin) {
709 return null;
710 }
711
712 if ($this->grademax == $this->grademin) {
713 return $this->grademax; // no range
714 }
715
716 // Standardise score to the new grade range
717 // NOTE: this is not compatible with current assignment grading
718 if ($rawmin != $this->grademin or $rawmax != $this->grademax) {
9a68cffc 719 $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax);
3c2e81ee 720 }
721
722 // Apply other grade_item factors
723 $rawgrade *= $this->multfactor;
724 $rawgrade += $this->plusfactor;
725
726 return bounded_number($this->grademin, $rawgrade, $this->grademax);
727
728 } else if ($this->gradetype == GRADE_TYPE_SCALE) { // Dealing with a scale value
729 if (empty($this->scale)) {
730 $this->load_scale();
731 }
732
733 if ($this->grademax < 0) {
734 return null; // scale not present - no grade
735 }
736
737 if ($this->grademax == 0) {
738 return $this->grademax; // only one option
739 }
740
741 // Convert scale if needed
742 // NOTE: this is not compatible with current assignment grading
743 if ($rawmin != $this->grademin or $rawmax != $this->grademax) {
9a68cffc 744 $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax);
3c2e81ee 745 }
746
747 return (int)bounded_number(0, round($rawgrade+0.00001), $this->grademax);
748
749
750 } else if ($this->gradetype == GRADE_TYPE_TEXT or $this->gradetype == GRADE_TYPE_NONE) { // no value
751 // somebody changed the grading type when grades already existed
752 return null;
753
754 } else {
755 dubugging("Unkown grade type");
756 return null;;
757 }
758 }
759
760 /**
761 * Sets this grade_item's needsupdate to true. Also marks the course item as needing update.
762 * @return void
763 */
764 function force_regrading() {
765 $this->needsupdate = 1;
766 //mark this item and course item only - categories and calculated items are always regraded
767 $wheresql = "(itemtype='course' OR id={$this->id}) AND courseid={$this->courseid}";
4fc9ec1e 768 $this->lib_wrapper->set_field_select('grade_items', 'needsupdate', 1, $wheresql);
3c2e81ee 769 }
770
771 /**
772 * Instantiates a grade_scale object whose data is retrieved from the DB,
773 * if this item's scaleid variable is set.
774 * @return object grade_scale or null if no scale used
775 */
776 function load_scale() {
777 if ($this->gradetype != GRADE_TYPE_SCALE) {
778 $this->scaleid = null;
779 }
780
781 if (!empty($this->scaleid)) {
782 //do not load scale if already present
783 if (empty($this->scale->id) or $this->scale->id != $this->scaleid) {
aaefeda4 784 $grade_scale = grade_object::get_instance('grade_scale');
4fc9ec1e 785 $this->scale = $grade_scale->fetch(array('id'=>$this->scaleid));
3c2e81ee 786 $this->scale->load_items();
787 }
788
789 // Until scales are uniformly set to min=0 max=count(scaleitems)-1 throughout Moodle, we
790 // stay with the current min=1 max=count(scaleitems)
791 $this->grademax = count($this->scale->scale_items);
792 $this->grademin = 1;
793
794 } else {
795 $this->scale = null;
796 }
797
798 return $this->scale;
799 }
800
801 /**
802 * Instantiates a grade_outcome object whose data is retrieved from the DB,
803 * if this item's outcomeid variable is set.
804 * @return object grade_outcome
805 */
806 function load_outcome() {
807 if (!empty($this->outcomeid)) {
aaefeda4 808 $grade_outcome = grade_object::get_instance('grade_outcome');
4fc9ec1e 809 $this->outcome = $grade_outcome->fetch(array('id'=>$this->outcomeid));
3c2e81ee 810 }
811 return $this->outcome;
812 }
813
814 /**
815 * Returns the grade_category object this grade_item belongs to (referenced by categoryid)
816 * or category attached to category item.
817 *
818 * @return mixed grade_category object if applicable, false if course item
819 */
820 function get_parent_category() {
821 if ($this->is_category_item() or $this->is_course_item()) {
822 return $this->get_item_category();
823
824 } else {
aaefeda4 825 $grade_category = grade_object::get_instance('grade_category');
4fc9ec1e 826 return $grade_category->fetch(array('id'=>$this->categoryid));
3c2e81ee 827 }
828 }
829
830 /**
831 * Calls upon the get_parent_category method to retrieve the grade_category object
832 * from the DB and assigns it to $this->parent_category. It also returns the object.
833 * @return object Grade_category
834 */
835 function load_parent_category() {
836 if (empty($this->parent_category->id)) {
837 $this->parent_category = $this->get_parent_category();
838 }
839 return $this->parent_category;
840 }
841
842 /**
843 * Returns the grade_category for category item
844 *
845 * @return mixed grade_category object if applicable, false otherwise
846 */
847 function get_item_category() {
848 if (!$this->is_course_item() and !$this->is_category_item()) {
849 return false;
850 }
aaefeda4 851 $grade_category = grade_object::get_instance('grade_category');
4fc9ec1e 852 return $grade_category->fetch(array('id'=>$this->iteminstance));
3c2e81ee 853 }
854
855 /**
856 * Calls upon the get_item_category method to retrieve the grade_category object
857 * from the DB and assigns it to $this->item_category. It also returns the object.
858 * @return object Grade_category
859 */
860 function load_item_category() {
861 if (empty($this->category->id)) {
862 $this->item_category = $this->get_item_category();
863 }
864 return $this->item_category;
865 }
866
867 /**
868 * Is the grade item associated with category?
869 * @return boolean
870 */
871 function is_category_item() {
872 return ($this->itemtype == 'category');
873 }
874
875 /**
876 * Is the grade item associated with course?
877 * @return boolean
878 */
879 function is_course_item() {
880 return ($this->itemtype == 'course');
881 }
882
883 /**
884 * Is this a manualy graded item?
885 * @return boolean
886 */
887 function is_manual_item() {
888 return ($this->itemtype == 'manual');
889 }
890
891 /**
892 * Is this an outcome item?
893 * @return boolean
894 */
895 function is_outcome_item() {
896 return !empty($this->outcomeid);
897 }
898
899 /**
0f392ff4 900 * Is the grade item external - associated with module, plugin or something else?
3c2e81ee 901 * @return boolean
902 */
0f392ff4 903 function is_external_item() {
904 return ($this->itemtype == 'mod');
905 }
906
907 /**
908 * Is the grade item overridable
909 * @return boolean
910 */
911 function is_overridable_item() {
912 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 913 }
914
915 /**
916 * Returns true if grade items uses raw grades
917 * @return boolean
918 */
919 function is_raw_used() {
0f392ff4 920 return ($this->is_external_item() and !$this->is_calculated() and !$this->is_outcome_item());
3c2e81ee 921 }
922
923 /**
924 * Returns grade item associated with the course
925 * @param int $courseid
926 * @return course item object
927 */
928 function fetch_course_item($courseid) {
aaefeda4 929 $obj = grade_object::get_instance('grade_item');;
4fc9ec1e 930 if ($course_item = $obj->fetch(array('courseid'=>$courseid, 'itemtype'=>'course'))) {
3c2e81ee 931 return $course_item;
932 }
933
934 // first get category - it creates the associated grade item
aaefeda4 935 $grade_category = grade_object::get_instance('grade_category');
4fc9ec1e 936 $course_category = $grade_category->fetch_course_category($courseid);
3c2e81ee 937
4fc9ec1e 938 return $obj->fetch(array('courseid'=>$courseid, 'itemtype'=>'course'));
3c2e81ee 939 }
940
941 /**
942 * Is grading object editable?
943 * @return boolean
944 */
945 function is_editable() {
946 return true;
947 }
948
949 /**
950 * Checks if grade calculated. Returns this object's calculation.
951 * @return boolean true if grade item calculated.
952 */
953 function is_calculated() {
954 if (empty($this->calculation)) {
955 return false;
956 }
957
958 /*
959 * The main reason why we use the ##gixxx## instead of [[idnumber]] is speed of depends_on(),
960 * we would have to fetch all course grade items to find out the ids.
961 * Also if user changes the idnumber the formula does not need to be updated.
962 */
963
964 // first detect if we need to change calculation formula from [[idnumber]] to ##giXXX## (after backup, etc.)
965 if (!$this->calculation_normalized and preg_match('/##gi\d+##/', $this->calculation)) {
966 $this->set_calculation($this->calculation);
967 }
968
969 return !empty($this->calculation);
970 }
971
972 /**
973 * Returns calculation string if grade calculated.
974 * @return mixed string if calculation used, null if not
975 */
976 function get_calculation() {
977 if ($this->is_calculated()) {
4fc9ec1e 978 return $this->denormalize_formula($this->calculation, $this->courseid);
3c2e81ee 979
980 } else {
981 return NULL;
982 }
983 }
984
985 /**
986 * Sets this item's calculation (creates it) if not yet set, or
987 * updates it if already set (in the DB). If no calculation is given,
988 * the calculation is removed.
989 * @param string $formula string representation of formula used for calculation
990 * @return boolean success
991 */
992 function set_calculation($formula) {
4fc9ec1e 993 $this->calculation = $this->normalize_formula($formula, $this->courseid);
3c2e81ee 994 $this->calculation_normalized = true;
995 return $this->update();
996 }
997
998 /**
999 * Denormalizes the calculation formula to [idnumber] form
1000 * @static
1001 * @param string $formula
1002 * @return string denormalized string
1003 */
1004 function denormalize_formula($formula, $courseid) {
1005 if (empty($formula)) {
1006 return '';
1007 }
1008
1009 // denormalize formula - convert ##giXX## to [[idnumber]]
1010 if (preg_match_all('/##gi(\d+)##/', $formula, $matches)) {
1011 foreach ($matches[1] as $id) {
aaefeda4 1012 $obj = grade_object::get_instance('grade_item');;
1013 if ($grade_item = $obj->fetch(array('id'=>$id, 'courseid'=>$courseid))) {
3c2e81ee 1014 if (!empty($grade_item->idnumber)) {
1015 $formula = str_replace('##gi'.$grade_item->id.'##', '[['.$grade_item->idnumber.']]', $formula);
1016 }
1017 }
1018 }
1019 }
1020
1021 return $formula;
1022
1023 }
1024
1025 /**
1026 * Normalizes the calculation formula to [#giXX#] form
1027 * @static
1028 * @param string $formula
1029 * @return string normalized string
1030 */
1031 function normalize_formula($formula, $courseid) {
1032 $formula = trim($formula);
1033
1034 if (empty($formula)) {
1035 return NULL;
1036
1037 }
1038
1039 // normalize formula - we want grade item ids ##giXXX## instead of [[idnumber]]
aaefeda4 1040 $obj = grade_object::get_instance('grade_item');;
1041 if ($grade_items = $obj->fetch_all(array('courseid'=>$courseid))) {
3c2e81ee 1042 foreach ($grade_items as $grade_item) {
1043 $formula = str_replace('[['.$grade_item->idnumber.']]', '##gi'.$grade_item->id.'##', $formula);
1044 }
1045 }
1046
1047 return $formula;
1048 }
1049
1050 /**
1051 * Returns the final values for this grade item (as imported by module or other source).
1052 * @param int $userid Optional: to retrieve a single final grade
1053 * @return mixed An array of all final_grades (stdClass objects) for this grade_item, or a single final_grade.
1054 */
1055 function get_final($userid=NULL) {
1056 if ($userid) {
4fc9ec1e 1057 if ($user = $this->lib_wrapper->get_record('grade_grades', 'itemid', $this->id, 'userid', $userid)) {
3c2e81ee 1058 return $user;
1059 }
1060
1061 } else {
4fc9ec1e 1062 if ($grades = $this->lib_wrapper->get_records('grade_grades', 'itemid', $this->id)) {
3c2e81ee 1063 //TODO: speed up with better SQL
1064 $result = array();
1065 foreach ($grades as $grade) {
1066 $result[$grade->userid] = $grade;
1067 }
1068 return $result;
1069 } else {
1070 return array();
1071 }
1072 }
1073 }
1074
1075 /**
1076 * Get (or create if not exist yet) grade for this user
1077 * @param int $userid
1078 * @return object grade_grade object instance
1079 */
1080 function get_grade($userid, $create=true) {
1081 if (empty($this->id)) {
1082 debugging('Can not use before insert');
1083 return false;
1084 }
1085
4fc9ec1e 1086 $grade = $this->get_instance('grade_grade', array('userid'=>$userid, 'itemid'=>$this->id));
3c2e81ee 1087 if (empty($grade->id) and $create) {
1088 $grade->insert();
1089 }
1090
1091 return $grade;
1092 }
1093
1094 /**
1095 * Returns the sortorder of this grade_item. This method is also available in
1096 * grade_category, for cases where the object type is not know.
1097 * @return int Sort order
1098 */
1099 function get_sortorder() {
1100 return $this->sortorder;
1101 }
1102
1103 /**
1104 * Returns the idnumber of this grade_item. This method is also available in
1105 * grade_category, for cases where the object type is not know.
1106 * @return string idnumber
1107 */
1108 function get_idnumber() {
1109 return $this->idnumber;
1110 }
1111
1112 /**
1113 * Returns this grade_item. This method is also available in
1114 * grade_category, for cases where the object type is not know.
1115 * @return string idnumber
1116 */
1117 function get_grade_item() {
1118 return $this;
1119 }
1120
1121 /**
1122 * Sets the sortorder of this grade_item. This method is also available in
1123 * grade_category, for cases where the object type is not know.
1124 * @param int $sortorder
1125 * @return void
1126 */
1127 function set_sortorder($sortorder) {
1128 $this->sortorder = $sortorder;
1129 $this->update();
1130 }
1131
1132 function move_after_sortorder($sortorder) {
1133 global $CFG;
1134
1135 //make some room first
1136 $sql = "UPDATE {$CFG->prefix}grade_items
1137 SET sortorder = sortorder + 1
1138 WHERE sortorder > $sortorder AND courseid = {$this->courseid}";
4fc9ec1e 1139 $this->lib_wrapper->execute_sql($sql, false);
3c2e81ee 1140
1141 $this->set_sortorder($sortorder + 1);
1142 }
1143
1144 /**
1145 * Returns the most descriptive field for this object. This is a standard method used
1146 * when we do not know the exact type of an object.
1147 * @return string name
1148 */
1149 function get_name() {
1150 if (!empty($this->itemname)) {
1151 // MDL-10557
1152 return format_string($this->itemname);
1153
1154 } else if ($this->is_course_item()) {
1155 return get_string('coursetotal', 'grades');
1156
1157 } else if ($this->is_category_item()) {
1158 return get_string('categorytotal', 'grades');
1159
1160 } else {
1161 return get_string('grade');
1162 }
1163 }
1164
1165 /**
1166 * Sets this item's categoryid. A generic method shared by objects that have a parent id of some kind.
1167 * @param int $parentid
1168 * @return boolean success;
1169 */
1170 function set_parent($parentid) {
1171 if ($this->is_course_item() or $this->is_category_item()) {
1172 error('Can not set parent for category or course item!');
1173 }
1174
1175 if ($this->categoryid == $parentid) {
1176 return true;
1177 }
1178
1179 // find parent and check course id
aaefeda4 1180 $grade_category = grade_object::get_instance('grade_category');
4fc9ec1e 1181 if (!$parent_category = $grade_category->fetch(array('id'=>$parentid, 'courseid'=>$this->courseid))) {
3c2e81ee 1182 return false;
1183 }
1184
1185 $this->force_regrading();
1186
1187 // set new parent
1188 $this->categoryid = $parent_category->id;
1189 $this->parent_category =& $parent_category;
1190
1191 return $this->update();
1192 }
1193
1194 /**
1195 * Finds out on which other items does this depend directly when doing calculation or category agregation
1196 * @param bool $reset_cache
1197 * @return array of grade_item ids this one depends on
1198 */
1199 function depends_on($reset_cache=false) {
1200 global $CFG;
1201
1202 if ($reset_cache) {
1203 $this->dependson_cache = null;
1204 } else if (isset($this->dependson_cache)) {
1205 return $this->dependson_cache;
1206 }
1207
1208 if ($this->is_locked()) {
1209 // locked items do not need to be regraded
1210 $this->dependson_cache = array();
1211 return $this->dependson_cache;
1212 }
1213
1214 if ($this->is_calculated()) {
1215 if (preg_match_all('/##gi(\d+)##/', $this->calculation, $matches)) {
1216 $this->dependson_cache = array_unique($matches[1]); // remove duplicates
1217 return $this->dependson_cache;
1218 } else {
1219 $this->dependson_cache = array();
1220 return $this->dependson_cache;
1221 }
1222
1223 } else if ($grade_category = $this->load_item_category()) {
1224 //only items with numeric or scale values can be aggregated
1225 if ($this->gradetype != GRADE_TYPE_VALUE and $this->gradetype != GRADE_TYPE_SCALE) {
1226 $this->dependson_cache = array();
1227 return $this->dependson_cache;
1228 }
1229
a6771652 1230 $grade_category->apply_forced_settings();
3c2e81ee 1231
1232 if (empty($CFG->enableoutcomes) or $grade_category->aggregateoutcomes) {
1233 $outcomes_sql = "";
1234 } else {
1235 $outcomes_sql = "AND gi.outcomeid IS NULL";
1236 }
1237
1238 if ($grade_category->aggregatesubcats) {
1239 // return all children excluding category items
1240 $sql = "SELECT gi.id
1241 FROM {$CFG->prefix}grade_items gi
1242 WHERE (gi.gradetype = ".GRADE_TYPE_VALUE." OR gi.gradetype = ".GRADE_TYPE_SCALE.")
1243 $outcomes_sql
1244 AND gi.categoryid IN (
1245 SELECT gc.id
1246 FROM {$CFG->prefix}grade_categories gc
1247 WHERE gc.path LIKE '%/{$grade_category->id}/%')";
1248
1249 } else {
1250 $sql = "SELECT gi.id
1251 FROM {$CFG->prefix}grade_items gi
1252 WHERE gi.categoryid = {$grade_category->id}
1253 AND (gi.gradetype = ".GRADE_TYPE_VALUE." OR gi.gradetype = ".GRADE_TYPE_SCALE.")
1254 $outcomes_sql
1255
1256 UNION
1257
1258 SELECT gi.id
1259 FROM {$CFG->prefix}grade_items gi, {$CFG->prefix}grade_categories gc
1260 WHERE (gi.itemtype = 'category' OR gi.itemtype = 'course') AND gi.iteminstance=gc.id
1261 AND gc.parent = {$grade_category->id}
1262 AND (gi.gradetype = ".GRADE_TYPE_VALUE." OR gi.gradetype = ".GRADE_TYPE_SCALE.")
1263 $outcomes_sql";
1264 }
1265
4fc9ec1e 1266 if ($children = $this->lib_wrapper->get_records_sql($sql)) {
3c2e81ee 1267 $this->dependson_cache = array_keys($children);
1268 return $this->dependson_cache;
1269 } else {
1270 $this->dependson_cache = array();
1271 return $this->dependson_cache;
1272 }
1273
1274 } else {
1275 $this->dependson_cache = array();
1276 return $this->dependson_cache;
1277 }
1278 }
1279
1280 /**
1994d890 1281 * Refetch grades from modules, plugins.
3c2e81ee 1282 * @param int $userid optional, one user only
1283 */
1284 function refresh_grades($userid=0) {
1285 if ($this->itemtype == 'mod') {
1286 if ($this->is_outcome_item()) {
1287 //nothing to do
1288 return;
1289 }
1290
4fc9ec1e 1291 if (!$activity = $this->lib_wrapper->get_record($this->itemmodule, 'id', $this->iteminstance)) {
1994d890 1292 debugging("Can not find $this->itemmodule activity with id $this->iteminstance");
3c2e81ee 1293 return;
1294 }
1295
4fc9ec1e 1296 if (!$cm = $this->lib_wrapper->get_coursemodule_from_instance($this->itemmodule, $activity->id, $this->courseid)) {
1994d890 1297 debugging('Can not find course module');
3c2e81ee 1298 return;
1299 }
1300
1301 $activity->modname = $this->itemmodule;
1302 $activity->cmidnumber = $cm->idnumber;
1303
1304 grade_update_mod_grades($activity);
1305 }
1306 }
1307
1308 /**
1309 * Updates final grade value for given user, this is a only way to update final
1310 * grades from gradebook and import because it logs the change in history table
1311 * and deals with overridden flag. This flag is set to prevent later overriding
1312 * from raw grades submitted from modules.
1313 *
1314 * @param int $userid the graded user
1315 * @param mixed $finalgrade float value of final grade - false means do not change
1316 * @param string $howmodified modification source
1317 * @param string $note optional note
1318 * @param mixed $feedback teachers feedback as string - false means do not change
1319 * @param int $feedbackformat
1320 * @return boolean success
1321 */
0f392ff4 1322 function update_final_grade($userid, $finalgrade=false, $source=NULL, $feedback=false, $feedbackformat=FORMAT_MOODLE, $usermodified=null) {
3c2e81ee 1323 global $USER, $CFG;
1324
3c2e81ee 1325 $result = true;
1326
1327 // no grading used or locked
1328 if ($this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) {
1329 return false;
1330 }
1331
4fc9ec1e 1332 $grade = $this->get_instance('grade_grade', array('itemid'=>$this->id, 'userid'=>$userid));
3c2e81ee 1333 $grade->grade_item =& $this; // prevent db fetching of this grade_item
1334
ced5ee59 1335 if (empty($usermodified)) {
1336 $grade->usermodified = $USER->id;
1337 } else {
1338 $grade->usermodified = $usermodified;
1339 }
3c2e81ee 1340
1341 if ($grade->is_locked()) {
1342 // do not update locked grades at all
1343 return false;
1344 }
1345
1346 $locktime = $grade->get_locktime();
1347 if ($locktime and $locktime < time()) {
1348 // do not update grades that should be already locked, force regrade instead
1349 $this->force_regrading();
1350 return false;
1351 }
1352
717f432f 1353 // we need proper floats here for !== comparison later
1354 if (!is_null($grade->finalgrade)) {
1355 $grade->finalgrade = (float)$grade->finalgrade;
1356 }
1357
3c2e81ee 1358 $oldgrade = new object();
1359 $oldgrade->finalgrade = $grade->finalgrade;
1360 $oldgrade->overridden = $grade->overridden;
1361 $oldgrade->feedback = $grade->feedback;
1362 $oldgrade->feedbackformat = $grade->feedbackformat;
1363
0f392ff4 1364 // changed grade?
1365 if ($finalgrade !== false) {
1366 if ($this->is_overridable_item()) {
3c2e81ee 1367 $grade->overridden = time();
0f392ff4 1368 } else {
1369 $grade->overridden = 0;
3c2e81ee 1370 }
3c2e81ee 1371
717f432f 1372 if (is_null($finalgrade)) {
1373 $grade->finalgrade = null;
3c2e81ee 1374 } else {
717f432f 1375 $grade->finalgrade = (float)bounded_number($this->grademin, $finalgrade, $this->grademax);
3c2e81ee 1376 }
3c2e81ee 1377 }
1378
1379 // do we have comment from teacher?
1380 if ($feedback !== false) {
0f392ff4 1381 if ($this->is_external_item()) {
1382 // external items (modules, plugins) may have own feedback
1383 $grade->overridden = time();
1384 }
1385
3c2e81ee 1386 $grade->feedback = $feedback;
1387 $grade->feedbackformat = $feedbackformat;
1388 }
1389
1390 if (empty($grade->id)) {
ced5ee59 1391 $grade->timecreated = null; // no submission yet
1392 $grade->timemodified = time(); // overridden flag might take over, but anyway
3c2e81ee 1393 $result = (boolean)$grade->insert($source);
1394
1395 } else if ($grade->finalgrade !== $oldgrade->finalgrade
1396 or $grade->feedback !== $oldgrade->feedback
1397 or $grade->feedbackformat !== $oldgrade->feedbackformat) {
ced5ee59 1398 $grade->timemodified = time(); // overridden flag might take over, but anyway
3c2e81ee 1399 $result = $grade->update($source);
0f392ff4 1400 } else {
1401 // no grade change
1402 return $result;
3c2e81ee 1403 }
1404
1405 if (!$result) {
1406 // something went wrong - better force final grade recalculation
1407 $this->force_regrading();
1408
1409 } else if ($this->is_course_item() and !$this->needsupdate) {
1410 if (!grade_regrade_final_grades($this->courseid, $userid, $this)) {
1411 $this->force_regrading();
1412 }
1413
1414 } else if (!$this->needsupdate) {
aaefeda4 1415 $obj = grade_object::get_instance('grade_item');;
1416 $course_item = $obj->fetch_course_item($this->courseid);
3c2e81ee 1417 if (!$course_item->needsupdate) {
1418 if (!grade_regrade_final_grades($this->courseid, $userid, $this)) {
1419 $this->force_regrading();
1420 }
1421 } else {
1422 $this->force_regrading();
1423 }
1424 }
1425
1426 return $result;
1427 }
1428
1429
1430 /**
1431 * Updates raw grade value for given user, this is a only way to update raw
1432 * grades from external source (modules, etc.),
1433 * because it logs the change in history table and deals with final grade recalculation.
1434 *
1435 * @param int $userid the graded user
1436 * @param mixed $rawgrade float value of raw grade - false means do not change
1437 * @param string $howmodified modification source
1438 * @param string $note optional note
1439 * @param mixed $feedback teachers feedback as string - false means do not change
1440 * @param int $feedbackformat
ced5ee59 1441 * @param int $usermodified - user which did the grading
1442 * @param int $dategraded
1443 * @param int $datesubmitted
3c2e81ee 1444 * @return boolean success
1445 */
ced5ee59 1446 function update_raw_grade($userid, $rawgrade=false, $source=NULL, $feedback=false, $feedbackformat=FORMAT_MOODLE, $usermodified=null, $dategraded=null, $datesubmitted=null) {
3c2e81ee 1447 global $USER;
1448
3c2e81ee 1449 $result = true;
1450
1451 // calculated grades can not be updated; course and category can not be updated because they are aggregated
0f392ff4 1452 if (!$this->is_raw_used() or $this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) {
3c2e81ee 1453 return false;
1454 }
1455
4fc9ec1e 1456 $grade = $this->get_instance('grade_grade', array('itemid'=>$this->id, 'userid'=>$userid));
3c2e81ee 1457 $grade->grade_item =& $this; // prevent db fetching of this grade_item
1458
ced5ee59 1459 if (empty($usermodified)) {
1460 $grade->usermodified = $USER->id;
1461 } else {
1462 $grade->usermodified = $usermodified;
1463 }
1464
1465 // TODO: hack alert - create new fields for these
1466
1467 $grade->timecreated = $datesubmitted;
1468
1469 if (empty($dategraded)) {
717f432f 1470 $grade->timemodified = time();
ced5ee59 1471 } else {
717f432f 1472 $grade->timemodified = $dategraded;
ced5ee59 1473 }
3c2e81ee 1474
1475 if ($grade->is_locked()) {
1476 // do not update locked grades at all
1477 return false;
1478 }
1479
1480 $locktime = $grade->get_locktime();
1481 if ($locktime and $locktime < time()) {
1482 // do not update grades that should be already locked and force regrade
1483 $this->force_regrading();
1484 return false;
1485 }
1486
717f432f 1487 // we need proper floats here for !== comparison later
1488 if (!is_null($grade->rawgrade)) {
1489 $grade->rawgrade = (float)$grade->rawgrade;
1490 }
1491
3c2e81ee 1492 $oldgrade = new object();
1493 $oldgrade->finalgrade = $grade->finalgrade;
1494 $oldgrade->rawgrade = $grade->rawgrade;
1495 $oldgrade->rawgrademin = $grade->rawgrademin;
1496 $oldgrade->rawgrademax = $grade->rawgrademax;
1497 $oldgrade->rawscaleid = $grade->rawscaleid;
1498 $oldgrade->feedback = $grade->feedback;
1499 $oldgrade->feedbackformat = $grade->feedbackformat;
1500
1501 // fist copy current grademin/max and scale
1502 $grade->rawgrademin = $this->grademin;
1503 $grade->rawgrademax = $this->grademax;
1504 $grade->rawscaleid = $this->scaleid;
1505
1506 // change raw grade?
1507 if ($rawgrade !== false) {
1508 $grade->rawgrade = $rawgrade;
1509 }
1510
1511 // do we have comment from teacher?
1512 if ($feedback !== false) {
1513 $grade->feedback = $feedback;
1514 $grade->feedbackformat = $feedbackformat;
1515 }
1516
717f432f 1517 if (is_null($grade->rawgrade)) {
1518 $grade->timemodified = null; // dategraded hack - not graded if no grade present, comments do not count here as grading
1519 }
1520
3c2e81ee 1521 if (empty($grade->id)) {
1522 $result = (boolean)$grade->insert($source);
1523
1524 } else if ($grade->finalgrade !== $oldgrade->finalgrade
1525 or $grade->rawgrade !== $oldgrade->rawgrade
1526 or $grade->rawgrademin !== $oldgrade->rawgrademin
1527 or $grade->rawgrademax !== $oldgrade->rawgrademax
1528 or $grade->rawscaleid !== $oldgrade->rawscaleid
1529 or $grade->feedback !== $oldgrade->feedback
1530 or $grade->feedbackformat !== $oldgrade->feedbackformat) {
3c2e81ee 1531 $result = $grade->update($source);
1532 }
1533
1534 if (!$result) {
1535 // something went wrong - better force final grade recalculation
1536 $this->force_regrading();
1537
1538 } else if (!$this->needsupdate) {
aaefeda4 1539 $obj = grade_object::get_instance('grade_item');;
1540 $course_item = $obj->fetch_course_item($this->courseid);
3c2e81ee 1541 if (!$course_item->needsupdate) {
1542 if (!grade_regrade_final_grades($this->courseid, $userid, $this)) {
1543 $this->force_regrading();
1544 }
1545 } else {
1546 $this->force_regrading();
1547 }
1548 }
1549
1550 return $result;
1551 }
1552
1553 /**
1554 * Calculates final grade values using the formula in calculation property.
1555 * The parameters are taken from final grades of grade items in current course only.
1556 * @return boolean false if error
1557 */
1558 function compute($userid=null) {
1559 global $CFG;
1560
1561 if (!$this->is_calculated()) {
1562 return false;
1563 }
1564
1565 require_once($CFG->libdir.'/mathslib.php');
1566
1567 if ($this->is_locked()) {
1568 return true; // no need to recalculate locked items
1569 }
1570
1571 // get used items
1572 $useditems = $this->depends_on();
1573
1574 // prepare formula and init maths library
1575 $formula = preg_replace('/##(gi\d+)##/', '\1', $this->calculation);
1576 $this->formula = new calc_formula($formula);
1577
1578 // where to look for final grades?
1579 // this itemid is added so that we use only one query for source and final grades
1580 $gis = implode(',', array_merge($useditems, array($this->id)));
1581
1582 if ($userid) {
1583 $usersql = "AND g.userid=$userid";
1584 } else {
1585 $usersql = "";
1586 }
1587
aaefeda4 1588 $grade_inst = grade_object::get_instance('grade_grade');
3c2e81ee 1589 $fields = 'g.'.implode(',g.', $grade_inst->required_fields);
1590
1591 $sql = "SELECT $fields
1592 FROM {$CFG->prefix}grade_grades g, {$CFG->prefix}grade_items gi
1593 WHERE gi.id = g.itemid AND gi.courseid={$this->courseid} AND gi.id IN ($gis) $usersql
1594 ORDER BY g.userid";
1595
1596 $return = true;
1597
1598 // group the grades by userid and use formula on the group
4fc9ec1e 1599 if ($rs = $this->lib_wrapper->get_recordset_sql($sql)) {
3c2e81ee 1600 $prevuser = 0;
1601 $grade_records = array();
1602 $oldgrade = null;
4fc9ec1e 1603 while ($used = $this->lib_wrapper->rs_fetch_next_record($rs)) {
3c2e81ee 1604 if ($used->userid != $prevuser) {
1605 if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {
1606 $return = false;
1607 }
1608 $prevuser = $used->userid;
1609 $grade_records = array();
1610 $oldgrade = null;
1611 }
1612 if ($used->itemid == $this->id) {
1613 $oldgrade = $used;
1614 }
1615 $grade_records['gi'.$used->itemid] = $used->finalgrade;
1616 }
1617 if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {
1618 $return = false;
1619 }
1620 }
4fc9ec1e 1621 $this->lib_wrapper->rs_close($rs);
3c2e81ee 1622
1623 return $return;
1624 }
1625
1626 /**
1627 * internal function - does the final grade calculation
1628 */
1629 function use_formula($userid, $params, $useditems, $oldgrade) {
1630 if (empty($userid)) {
1631 return true;
1632 }
1633
1634 // add missing final grade values
1635 // not graded (null) is counted as 0 - the spreadsheet way
1636 foreach($useditems as $gi) {
1637 if (!array_key_exists('gi'.$gi, $params)) {
1638 $params['gi'.$gi] = 0;
1639 } else {
1640 $params['gi'.$gi] = (float)$params['gi'.$gi];
1641 }
1642 }
1643
1644 // can not use own final grade during calculation
1645 unset($params['gi'.$this->id]);
1646
1647 // insert final grade - will be needed later anyway
1648 if ($oldgrade) {
4ac209d5 1649 if (is_null($oldgrade->finalgrade)) {
1650 $oldfinalgrade = null;
1651 } else {
1652 // we need proper floats here for !== comparison later
1653 $oldfinalgrade = (float)$oldgrade->finalgrade;
1654 }
4fc9ec1e 1655 $grade = $this->get_instance('grade_grade', $oldgrade, false); // fetching from db is not needed
3c2e81ee 1656 $grade->grade_item =& $this;
1657
1658 } else {
4fc9ec1e 1659 $grade = $this->get_instance('grade_grade', array('itemid'=>$this->id, 'userid'=>$userid), false);
3c2e81ee 1660 $grade->grade_item =& $this;
4ac209d5 1661 $grade->insert('system');
1662 $oldfinalgrade = null;
3c2e81ee 1663 }
1664
1665 // no need to recalculate locked or overridden grades
1666 if ($grade->is_locked() or $grade->is_overridden()) {
1667 return true;
1668 }
1669
1670 // do the calculation
1671 $this->formula->set_params($params);
1672 $result = $this->formula->evaluate();
1673
3c2e81ee 1674 if ($result === false) {
1675 $grade->finalgrade = null;
1676
1677 } else {
1678 // normalize
1679 $result = bounded_number($this->grademin, $result, $this->grademax);
1680 if ($this->gradetype == GRADE_TYPE_SCALE) {
1681 $result = round($result+0.00001); // round scales upwards
1682 }
4ac209d5 1683 $grade->finalgrade = (float)$result;
3c2e81ee 1684 }
1685
1686 // update in db if changed
4ac209d5 1687 if ($grade->finalgrade !== $oldfinalgrade) {
1688 $grade->update('compute');
3c2e81ee 1689 }
1690
1691 if ($result !== false) {
1692 //lock grade if needed
1693 }
1694
1695 if ($result === false) {
1696 return false;
1697 } else {
1698 return true;
1699 }
1700
1701 }
1702
1703 /**
1704 * Validate the formula.
1705 * @param string $formula
1706 * @return boolean true if calculation possible, false otherwise
1707 */
1708 function validate_formula($formulastr) {
1709 global $CFG;
1710 require_once($CFG->libdir.'/mathslib.php');
1711
4fc9ec1e 1712 $formulastr = $this->normalize_formula($formulastr, $this->courseid);
3c2e81ee 1713
1714 if (empty($formulastr)) {
1715 return true;
1716 }
1717
1718 if (strpos($formulastr, '=') !== 0) {
1719 return get_string('errorcalculationnoequal', 'grades');
1720 }
1721
1722 // get used items
1723 if (preg_match_all('/##gi(\d+)##/', $formulastr, $matches)) {
1724 $useditems = array_unique($matches[1]); // remove duplicates
1725 } else {
1726 $useditems = array();
1727 }
ced5ee59 1728
26d7de8b 1729 // MDL-11902
1730 // unset the value if formula is trying to reference to itself
1731 // but array keys does not match itemid
3c2e81ee 1732 if (!empty($this->id)) {
26d7de8b 1733 $useditems = array_diff($useditems, array($this->id));
1734 //unset($useditems[$this->id]);
3c2e81ee 1735 }
1736
1737 // prepare formula and init maths library
1738 $formula = preg_replace('/##(gi\d+)##/', '\1', $formulastr);
1739 $formula = new calc_formula($formula);
1740
1741
1742 if (empty($useditems)) {
1743 $grade_items = array();
1744
1745 } else {
1746 $gis = implode(',', $useditems);
1747
1748 $sql = "SELECT gi.*
1749 FROM {$CFG->prefix}grade_items gi
1750 WHERE gi.id IN ($gis) and gi.courseid={$this->courseid}"; // from the same course only!
1751
4fc9ec1e 1752 if (!$grade_items = $this->lib_wrapper->get_records_sql($sql)) {
3c2e81ee 1753 $grade_items = array();
1754 }
1755 }
1756
1757 $params = array();
1758 foreach ($useditems as $itemid) {
1759 // make sure all grade items exist in this course
1760 if (!array_key_exists($itemid, $grade_items)) {
1761 return false;
1762 }
1763 // use max grade when testing formula, this should be ok in 99.9%
1764 // division by 0 is one of possible problems
1765 $params['gi'.$grade_items[$itemid]->id] = $grade_items[$itemid]->grademax;
1766 }
1767
1768 // do the calculation
1769 $formula->set_params($params);
1770 $result = $formula->evaluate();
1771
1772 // false as result indicates some problem
1773 if ($result === false) {
1774 // TODO: add more error hints
1775 return get_string('errorcalculationunknown', 'grades');
1776 } else {
1777 return true;
1778 }
1779 }
1780
1781 /**
1782 * 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.
1783 * @return int Display type
1784 */
1785 function get_displaytype() {
1786 global $CFG;
1787
1788 if ($this->display == GRADE_DISPLAY_TYPE_DEFAULT) {
1789 return grade_get_setting($this->courseid, 'displaytype', $CFG->grade_displaytype);
1790
1791 } else {
1792 return $this->display;
1793 }
1794 }
1795
1796 /**
1797 * 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.
1798 * @return int Decimals (0 - 5)
1799 */
1800 function get_decimals() {
1801 global $CFG;
1802
1803 if (is_null($this->decimals)) {
1804 return grade_get_setting($this->courseid, 'decimalpoints', $CFG->grade_decimalpoints);
1805
1806 } else {
1807 return $this->decimals;
1808 }
1809 }
1810}
1811?>