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