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