MDL-40697 core_grades: trigger the grade_deleted event
[moodle.git] / lib / grade / grade_grade.php
CommitLineData
26f0525f 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 an individual user's grade
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
PS
24 */
25
26defined('MOODLE_INTERNAL') || die();
e5c674f1 27
28require_once('grade_object.php');
29
a153c9f2
AD
30/**
31 * grade_grades is an object mapped to DB table {prefix}grade_grades
32 *
33 * @package core_grades
34 * @category grade
35 * @copyright 2006 Nicolas Connault
36 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
37 */
3ee5c201 38class grade_grade extends grade_object {
4cf1b9be 39
e5c674f1 40 /**
7c8a963f 41 * The DB table.
e5c674f1 42 * @var string $table
43 */
da3801e8 44 public $table = 'grade_grades';
4cf1b9be 45
e5c674f1 46 /**
3f2b0c8a 47 * Array of required table fields, must start with 'id'.
48 * @var array $required_fields
e5c674f1 49 */
da3801e8 50 public $required_fields = array('id', 'itemid', 'userid', 'rawgrade', 'rawgrademax', 'rawgrademin',
3f2b0c8a 51 'rawscaleid', 'usermodified', 'finalgrade', 'hidden', 'locked',
bfe969e8 52 'locktime', 'exported', 'overridden', 'excluded', 'timecreated',
a1740d7b 53 'timemodified', 'aggregationstatus', 'aggregationweight');
3f2b0c8a 54
55 /**
56 * Array of optional fields with default values (these should match db defaults)
57 * @var array $optional_fields
58 */
da3801e8 59 public $optional_fields = array('feedback'=>null, 'feedbackformat'=>0, 'information'=>null, 'informationformat'=>0);
4cf1b9be 60
e5c674f1 61 /**
ac9b0805 62 * The id of the grade_item this grade belongs to.
e5c674f1 63 * @var int $itemid
64 */
da3801e8 65 public $itemid;
8f4a626d 66
67 /**
68 * The grade_item object referenced by $this->itemid.
a153c9f2 69 * @var grade_item $grade_item
8f4a626d 70 */
da3801e8 71 public $grade_item;
4cf1b9be 72
e5c674f1 73 /**
ac9b0805 74 * The id of the user this grade belongs to.
e5c674f1 75 * @var int $userid
76 */
da3801e8 77 public $userid;
4cf1b9be 78
e5c674f1 79 /**
80 * The grade value of this raw grade, if such was provided by the module.
ac9b0805 81 * @var float $rawgrade
e5c674f1 82 */
da3801e8 83 public $rawgrade;
4cf1b9be 84
e5c674f1 85 /**
86 * The maximum allowable grade when this grade was created.
ac9b0805 87 * @var float $rawgrademax
e5c674f1 88 */
da3801e8 89 public $rawgrademax = 100;
e5c674f1 90
91 /**
92 * The minimum allowable grade when this grade was created.
ac9b0805 93 * @var float $rawgrademin
e5c674f1 94 */
da3801e8 95 public $rawgrademin = 0;
e5c674f1 96
97 /**
98 * id of the scale, if this grade is based on a scale.
ac9b0805 99 * @var int $rawscaleid
e5c674f1 100 */
da3801e8 101 public $rawscaleid;
d5bdb228 102
e5c674f1 103 /**
104 * The userid of the person who last modified this grade.
105 * @var int $usermodified
106 */
da3801e8 107 public $usermodified;
e5c674f1 108
ac9b0805 109 /**
110 * The final value of this grade.
111 * @var float $finalgrade
112 */
da3801e8 113 public $finalgrade;
ac9b0805 114
115 /**
116 * 0 if visible, 1 always hidden or date not visible until
117 * @var float $hidden
118 */
da3801e8 119 public $hidden = 0;
ac9b0805 120
121 /**
122 * 0 not locked, date when the item was locked
123 * @var float locked
124 */
da3801e8 125 public $locked = 0;
ac9b0805 126
127 /**
128 * 0 no automatic locking, date when to lock the grade automatically
129 * @var float $locktime
130 */
da3801e8 131 public $locktime = 0;
ac9b0805 132
133 /**
134 * Exported flag
a153c9f2 135 * @var bool $exported
ac9b0805 136 */
da3801e8 137 public $exported = 0;
4cf1b9be 138
c86caae7 139 /**
140 * Overridden flag
a153c9f2 141 * @var bool $overridden
c86caae7 142 */
da3801e8 143 public $overridden = 0;
c86caae7 144
23207a1a 145 /**
146 * Grade excluded from aggregation functions
a153c9f2 147 * @var bool $excluded
23207a1a 148 */
da3801e8 149 public $excluded = 0;
23207a1a 150
ced5ee59 151 /**
a153c9f2
AD
152 * TODO: HACK: create a new field datesubmitted - the date of submission if any (MDL-31377)
153 * @var bool $timecreated
ced5ee59 154 */
da3801e8 155 public $timecreated = null;
ced5ee59 156
157 /**
a153c9f2
AD
158 * TODO: HACK: create a new field dategraded - the date of grading (MDL-31378)
159 * @var bool $timemodified
ced5ee59 160 */
da3801e8 161 public $timemodified = null;
ced5ee59 162
bfe969e8 163 /**
a1740d7b
DW
164 * Aggregation status flag. Can be one of 'unknown', 'dropped', 'novalue' or 'used'.
165 * @var string $aggregationstatus
bfe969e8 166 */
a1740d7b 167 public $aggregationstatus = 'unknown';
bfe969e8 168
a1740d7b
DW
169 /**
170 * Aggregation weight is the specific weight used in the aggregation calculation for this grade.
171 * @var float $aggregationweight
172 */
173 public $aggregationweight = null;
fcac8e51 174
175 /**
a153c9f2
AD
176 * Returns array of grades for given grade_item+users
177 *
178 * @param grade_item $grade_item
fcac8e51 179 * @param array $userids
d297269d 180 * @param bool $include_missing include grades that do not exist yet
fcac8e51 181 * @return array userid=>grade_grade array
182 */
22a9b6d8 183 public static function fetch_users_grades($grade_item, $userids, $include_missing=true) {
da3801e8 184 global $DB;
f3ac8eb4 185
fcac8e51 186 // hmm, there might be a problem with length of sql query
187 // if there are too many users requested - we might run out of memory anyway
188 $limit = 2000;
189 $count = count($userids);
190 if ($count > $limit) {
191 $half = (int)($count/2);
192 $first = array_slice($userids, 0, $half);
193 $second = array_slice($userids, $half);
f3ac8eb4 194 return grade_grade::fetch_users_grades($grade_item, $first, $include_missing) + grade_grade::fetch_users_grades($grade_item, $second, $include_missing);
fcac8e51 195 }
196
9718765e 197 list($user_ids_cvs, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'uid0');
198 $params['giid'] = $grade_item->id;
fcac8e51 199 $result = array();
9718765e 200 if ($grade_records = $DB->get_records_select('grade_grades', "itemid=:giid AND userid $user_ids_cvs", $params)) {
fcac8e51 201 foreach ($grade_records as $record) {
f3ac8eb4 202 $result[$record->userid] = new grade_grade($record, false);
fcac8e51 203 }
204 }
205 if ($include_missing) {
206 foreach ($userids as $userid) {
207 if (!array_key_exists($userid, $result)) {
f3ac8eb4 208 $grade_grade = new grade_grade();
fcac8e51 209 $grade_grade->userid = $userid;
210 $grade_grade->itemid = $grade_item->id;
211 $result[$userid] = $grade_grade;
212 }
213 }
214 }
215
216 return $result;
217 }
218
8f4a626d 219 /**
a153c9f2
AD
220 * Loads the grade_item object referenced by $this->itemid and saves it as $this->grade_item for easy access
221 *
222 * @return grade_item The grade_item instance referenced by $this->itemid
8f4a626d 223 */
da3801e8 224 public function load_grade_item() {
fb0e3570 225 if (empty($this->itemid)) {
226 debugging('Missing itemid');
227 $this->grade_item = null;
228 return null;
229 }
230
231 if (empty($this->grade_item)) {
f3ac8eb4 232 $this->grade_item = grade_item::fetch(array('id'=>$this->itemid));
fb0e3570 233
234 } else if ($this->grade_item->id != $this->itemid) {
235 debugging('Itemid mismatch');
f3ac8eb4 236 $this->grade_item = grade_item::fetch(array('id'=>$this->itemid));
8f4a626d 237 }
fb0e3570 238
8f4a626d 239 return $this->grade_item;
46566dd8 240 }
e5c674f1 241
79eabc2a 242 /**
243 * Is grading object editable?
a153c9f2
AD
244 *
245 * @return bool
79eabc2a 246 */
da3801e8 247 public function is_editable() {
d7ef47b3 248 if ($this->is_locked()) {
79eabc2a 249 return false;
250 }
251
d7ef47b3 252 $grade_item = $this->load_grade_item();
79eabc2a 253
254 if ($grade_item->gradetype == GRADE_TYPE_NONE) {
255 return false;
256 }
257
0e999796
AD
258 if ($grade_item->is_course_item() or $grade_item->is_category_item()) {
259 return (bool)get_config('moodle', 'grade_overridecat');
260 }
261
79eabc2a 262 return true;
263 }
264
2cc4b0f9 265 /**
266 * Check grade lock status. Uses both grade item lock and grade lock.
267 * Internally any date in locked field (including future ones) means locked,
268 * the date is stored for logging purposes only.
269 *
a153c9f2 270 * @return bool True if locked, false if not
2cc4b0f9 271 */
da3801e8 272 public function is_locked() {
d7ef47b3 273 $this->load_grade_item();
1f0e4921 274 if (empty($this->grade_item)) {
275 return !empty($this->locked);
276 } else {
277 return !empty($this->locked) or $this->grade_item->is_locked();
278 }
2cc4b0f9 279 }
280
23207a1a 281 /**
282 * Checks if grade overridden
a153c9f2
AD
283 *
284 * @return bool True if grade is overriden
23207a1a 285 */
da3801e8 286 public function is_overridden() {
c86caae7 287 return !empty($this->overridden);
288 }
289
ced5ee59 290 /**
a153c9f2
AD
291 * Returns timestamp of submission related to this grade, null if not submitted.
292 *
293 * @return int Timestamp
ced5ee59 294 */
da3801e8 295 public function get_datesubmitted() {
a153c9f2 296 //TODO: HACK - create new fields (MDL-31379)
ced5ee59 297 return $this->timecreated;
298 }
299
a1740d7b
DW
300 /**
301 * Returns the weight this grade contributed to the aggregated grade
302 *
303 * @return float|null
304 */
305 public function get_aggregationweight() {
306 return $this->aggregationweight;
307 }
308
309 /**
310 * Set aggregationweight.
311 *
312 * @param float $aggregationweight
313 * @return void
314 */
315 public function set_aggregationweight($aggregationweight) {
316 $this->aggregationweight = $aggregationweight;
317 $this->update();
318 }
319
bfe969e8
DW
320 /**
321 * Returns the info on how this value was used in the aggregated grade
322 *
6077a4d4 323 * @return string One of 'dropped', 'excluded', 'novalue', 'used' or 'extra'
bfe969e8 324 */
a1740d7b
DW
325 public function get_aggregationstatus() {
326 return $this->aggregationstatus;
bfe969e8
DW
327 }
328
329 /**
a1740d7b 330 * Set aggregationstatus flag
bfe969e8 331 *
a1740d7b 332 * @param string $aggregationstatus
bfe969e8
DW
333 * @return void
334 */
a1740d7b
DW
335 public function set_aggregationstatus($aggregationstatus) {
336 $this->aggregationstatus = $aggregationstatus;
bfe969e8
DW
337 $this->update();
338 }
339
ced5ee59 340 /**
a153c9f2
AD
341 * Returns timestamp when last graded, null if no grade present
342 *
ced5ee59 343 * @return int
344 */
da3801e8 345 public function get_dategraded() {
a153c9f2 346 //TODO: HACK - create new fields (MDL-31379)
cf12f6c5 347 if (is_null($this->finalgrade) and is_null($this->feedback)) {
ced5ee59 348 return null; // no grade == no date
349 } else if ($this->overridden) {
350 return $this->overridden;
351 } else {
352 return $this->timemodified;
353 }
354 }
355
23207a1a 356 /**
357 * Set the overridden status of grade
a153c9f2
AD
358 *
359 * @param bool $state requested overridden state
360 * @param bool $refresh refresh grades from external activities if needed
361 * @return bool true is db state changed
23207a1a 362 */
da3801e8 363 public function set_overridden($state, $refresh = true) {
23207a1a 364 if (empty($this->overridden) and $state) {
365 $this->overridden = time();
366 $this->update();
367 return true;
368
369 } else if (!empty($this->overridden) and !$state) {
370 $this->overridden = 0;
371 $this->update();
0f392ff4 372
373 if ($refresh) {
374 //refresh when unlocking
375 $this->grade_item->refresh_grades($this->userid);
376 }
377
23207a1a 378 return true;
379 }
380 return false;
381 }
382
383 /**
384 * Checks if grade excluded from aggregation functions
a153c9f2
AD
385 *
386 * @return bool True if grade is excluded from aggregation
23207a1a 387 */
da3801e8 388 public function is_excluded() {
23207a1a 389 return !empty($this->excluded);
390 }
391
392 /**
393 * Set the excluded status of grade
a153c9f2
AD
394 *
395 * @param bool $state requested excluded state
396 * @return bool True is database state changed
23207a1a 397 */
da3801e8 398 public function set_excluded($state) {
23207a1a 399 if (empty($this->excluded) and $state) {
400 $this->excluded = time();
401 $this->update();
402 return true;
403
404 } else if (!empty($this->excluded) and !$state) {
405 $this->excluded = 0;
406 $this->update();
407 return true;
408 }
409 return false;
410 }
411
2cc4b0f9 412 /**
388234f4 413 * Lock/unlock this grade.
2cc4b0f9 414 *
a153c9f2
AD
415 * @param int $lockedstate 0, 1 or a timestamp int(10) after which date the item will be locked.
416 * @param bool $cascade Ignored param
417 * @param bool $refresh Refresh grades when unlocking
418 * @return bool True if successful, false if can not set new lock state for grade
2cc4b0f9 419 */
da3801e8 420 public function set_locked($lockedstate, $cascade=false, $refresh=true) {
2cc4b0f9 421 $this->load_grade_item();
422
423 if ($lockedstate) {
2cc4b0f9 424 if ($this->grade_item->needsupdate) {
425 //can not lock grade if final not calculated!
426 return false;
427 }
428
429 $this->locked = time();
430 $this->update();
431
432 return true;
433
434 } else {
fb0e3570 435 if (!empty($this->locked) and $this->locktime < time()) {
436 //we have to reset locktime or else it would lock up again
437 $this->locktime = 0;
2cc4b0f9 438 }
439
440 // remove the locked flag
441 $this->locked = 0;
2cc4b0f9 442 $this->update();
443
25bcd908 444 if ($refresh and !$this->is_overridden()) {
445 //refresh when unlocking and not overridden
2b0f65e2 446 $this->grade_item->refresh_grades($this->userid);
447 }
448
2cc4b0f9 449 return true;
450 }
451 }
452
fb0e3570 453 /**
a153c9f2
AD
454 * Lock the grade if needed. Make sure this is called only when final grades are valid
455 *
ddc20982 456 * @param array $items array of all grade item ids
fb0e3570 457 * @return void
458 */
f20edd52 459 public static function check_locktime_all($items) {
da3801e8 460 global $CFG, $DB;
fb0e3570 461
fb0e3570 462 $now = time(); // no rounding needed, this is not supposed to be called every 10 seconds
5b0af8c5 463 list($usql, $params) = $DB->get_in_or_equal($items);
464 $params[] = $now;
1b42e677
EL
465 $rs = $DB->get_recordset_select('grade_grades', "itemid $usql AND locked = 0 AND locktime > 0 AND locktime < ?", $params);
466 foreach ($rs as $grade) {
467 $grade_grade = new grade_grade($grade, false);
468 $grade_grade->locked = time();
469 $grade_grade->update('locktime');
fb0e3570 470 }
1b42e677 471 $rs->close();
fb0e3570 472 }
473
474 /**
475 * Set the locktime for this grade.
476 *
477 * @param int $locktime timestamp for lock to activate
478 * @return void
479 */
da3801e8 480 public function set_locktime($locktime) {
fb0e3570 481 $this->locktime = $locktime;
482 $this->update();
483 }
7e3c9767 484
fb0e3570 485 /**
a153c9f2 486 * Get the locktime for this grade.
fb0e3570 487 *
488 * @return int $locktime timestamp for lock to activate
489 */
da3801e8 490 public function get_locktime() {
fb0e3570 491 $this->load_grade_item();
7e3c9767 492
fb0e3570 493 $item_locktime = $this->grade_item->get_locktime();
7e3c9767 494
fb0e3570 495 if (empty($this->locktime) or ($item_locktime and $item_locktime < $this->locktime)) {
496 return $item_locktime;
497
498 } else {
499 return $this->locktime;
7e3c9767 500 }
501 }
502
22e23c78 503 /**
f60c61b1 504 * Check grade hidden status. Uses data from both grade item and grade.
a153c9f2
AD
505 *
506 * @return bool true if hidden, false if not
22e23c78 507 */
da3801e8 508 public function is_hidden() {
f60c61b1 509 $this->load_grade_item();
1f0e4921 510 if (empty($this->grade_item)) {
511 return $this->hidden == 1 or ($this->hidden != 0 and $this->hidden > time());
512 } else {
513 return $this->hidden == 1 or ($this->hidden != 0 and $this->hidden > time()) or $this->grade_item->is_hidden();
da3801e8 514 }
f60c61b1 515 }
516
597f50e6 517 /**
518 * Check grade hidden status. Uses data from both grade item and grade.
a153c9f2
AD
519 *
520 * @return bool true if hiddenuntil, false if not
597f50e6 521 */
da3801e8 522 public function is_hiddenuntil() {
597f50e6 523 $this->load_grade_item();
524
525 if ($this->hidden == 1 or $this->grade_item->hidden == 1) {
526 return false; //always hidden
527 }
528
529 if ($this->hidden > 1 or $this->grade_item->hidden > 1) {
530 return true;
531 }
532
533 return false;
534 }
535
f60c61b1 536 /**
537 * Check grade hidden status. Uses data from both grade item and grade.
a153c9f2 538 *
f60c61b1 539 * @return int 0 means visible, 1 hidden always, timestamp hidden until
540 */
da3801e8 541 public function get_hidden() {
f60c61b1 542 $this->load_grade_item();
543
544 $item_hidden = $this->grade_item->get_hidden();
545
546 if ($item_hidden == 1) {
547 return 1;
22e23c78 548
f60c61b1 549 } else if ($item_hidden == 0) {
550 return $this->hidden;
551
552 } else {
553 if ($this->hidden == 0) {
554 return $item_hidden;
555 } else if ($this->hidden == 1) {
556 return 1;
557 } else if ($this->hidden > $item_hidden) {
558 return $this->hidden;
559 } else {
560 return $item_hidden;
561 }
562 }
22e23c78 563 }
564
565 /**
566 * Set the hidden status of grade, 0 mean visible, 1 always hidden, number means date to hide until.
a153c9f2 567 *
22e23c78 568 * @param int $hidden new hidden status
a153c9f2 569 * @param bool $cascade ignored
22e23c78 570 */
da3801e8 571 public function set_hidden($hidden, $cascade=false) {
22e23c78 572 $this->hidden = $hidden;
573 $this->update();
574 }
575
e5c674f1 576 /**
3ee5c201 577 * Finds and returns a grade_grade instance based on params.
61c33818 578 *
f92dcad8 579 * @param array $params associative arrays varname=>value
a153c9f2 580 * @return grade_grade Returns a grade_grade instance or false if none found
f92dcad8 581 */
da3801e8 582 public static function fetch($params) {
f3ac8eb4 583 return grade_object::fetch_helper('grade_grades', 'grade_grade', $params);
f92dcad8 584 }
61c33818 585
f92dcad8 586 /**
3ee5c201 587 * Finds and returns all grade_grade instances based on params.
f92dcad8 588 *
589 * @param array $params associative arrays varname=>value
f7d515b6 590 * @return array array of grade_grade instances or false if none found.
f92dcad8 591 */
da3801e8 592 public static function fetch_all($params) {
f3ac8eb4 593 return grade_object::fetch_all_helper('grade_grades', 'grade_grade', $params);
4cf1b9be 594 }
595
a8995b34 596 /**
ac9b0805 597 * Given a float value situated between a source minimum and a source maximum, converts it to the
598 * corresponding value situated between a target minimum and a target maximum. Thanks to Darlene
599 * for the formula :-)
a8995b34 600 *
ac9b0805 601 * @param float $rawgrade
602 * @param float $source_min
603 * @param float $source_max
604 * @param float $target_min
605 * @param float $target_max
606 * @return float Converted value
8f4a626d 607 */
da3801e8 608 public static function standardise_score($rawgrade, $source_min, $source_max, $target_min, $target_max) {
9580a21f 609 if (is_null($rawgrade)) {
ba74762b 610 return null;
9580a21f 611 }
612
2e0d37fe 613 if ($source_max == $source_min or $target_min == $target_max) {
614 // prevent division by 0
615 return $target_max;
616 }
617
ac9b0805 618 $factor = ($rawgrade - $source_min) / ($source_max - $source_min);
619 $diff = $target_max - $target_min;
620 $standardised_value = $factor * $diff + $target_min;
621 return $standardised_value;
6c76ea8d 622 }
6391ebe7 623
0db54b5b
DW
624 /**
625 * Given an array like this:
626 * $a = array(1=>array(2, 3),
627 * 2=>array(4),
628 * 3=>array(1),
629 * 4=>array())
630 * this function fully resolves the dependencies so each value will be an array of
631 * the all items this item depends on and their dependencies (and their dependencies...).
632 * It should not explode if there are circular dependencies.
633 * The dependency depth array will list the number of branches in the tree above each leaf.
634 *
635 * @param array $dependson Array to flatten
636 * @param array $dependencydepth Array of itemids => depth. Initially these should be all set to 1.
637 * @return array Flattened array
638 */
639 protected static function flatten_dependencies_array(&$dependson, &$dependencydepth) {
640 // Flatten the nested dependencies - this will handle recursion bombs because it removes duplicates.
641 $somethingchanged = true;
642 while ($somethingchanged) {
643 $somethingchanged = false;
644
645 foreach ($dependson as $itemid => $depends) {
646 // Make a copy so we can tell if it changed.
647 $before = $dependson[$itemid];
648 foreach ($depends as $subitemid => $subdepends) {
649 $dependson[$itemid] = array_unique(array_merge($depends, $dependson[$subdepends]));
650 sort($dependson[$itemid], SORT_NUMERIC);
651 }
652 if ($before != $dependson[$itemid]) {
653 $somethingchanged = true;
654 if (!isset($dependencydepth[$itemid])) {
655 $dependencydepth[$itemid] = 1;
656 } else {
657 $dependencydepth[$itemid]++;
658 }
659 }
660 }
661 }
662 }
663
6391ebe7 664 /**
665 * Return array of grade item ids that are either hidden or indirectly depend
666 * on hidden grades, excluded grades are not returned.
d297269d 667 * THIS IS A REALLY BIG HACK! to be replaced by conditional aggregation of hidden grades in 2.0
668 *
a153c9f2
AD
669 * @param array $grade_grades all course grades of one user, & used for better internal caching
670 * @param array $grade_items array of grade items, & used for better internal caching
5232d3f2
DW
671 * @return array This is an array of 3 arrays:
672 * unknown => list of item ids that may be affected by hiding (with the calculated grade as the value)
673 * altered => list of item ids that are definitely affected by hiding (with the calculated grade as the value)
674 * alteredgrademax => for each item in altered or unknown, the new value of the grademax
6070e533
DW
675 * alteredgrademin => for each item in altered or unknown, the new value of the grademin
676 * alteredgradestatus => for each item with a modified status - the value of the new status
677 * alteredgradeweight => for each item with a modified weight - the value of the new weight
6391ebe7 678 */
da3801e8 679 public static function get_hiding_affected(&$grade_grades, &$grade_items) {
d297269d 680 global $CFG;
681
6391ebe7 682 if (count($grade_grades) !== count($grade_items)) {
2f137aa1 683 print_error('invalidarraysize', 'debug', '', 'grade_grade::get_hiding_affected()!');
6391ebe7 684 }
685
686 $dependson = array();
d297269d 687 $todo = array();
688 $unknown = array(); // can not find altered
689 $altered = array(); // altered grades
5232d3f2
DW
690 $alteredgrademax = array(); // Altered grade max values.
691 $alteredgrademin = array(); // Altered grade min values.
53771c40
DW
692 $alteredaggregationstatus = array(); // Altered aggregation status.
693 $alteredaggregationweight = array(); // Altered aggregation weight.
0db54b5b 694 $dependencydepth = array();
d297269d 695
d297269d 696 $hiddenfound = false;
4c8893ed 697 foreach($grade_grades as $itemid=>$unused) {
698 $grade_grade =& $grade_grades[$itemid];
0db54b5b
DW
699 // We need the immediate dependencies of all every grade_item so we can calculate nested dependencies.
700 $dependson[$grade_grade->itemid] = $grade_items[$grade_grade->itemid]->depends_on();
d297269d 701 if ($grade_grade->is_excluded()) {
702 //nothing to do, aggregation is ok
703 } else if ($grade_grade->is_hidden()) {
704 $hiddenfound = true;
ced5ee59 705 $altered[$grade_grade->itemid] = null;
4c8893ed 706 } else if ($grade_grade->is_locked() or $grade_grade->is_overridden()) {
707 // no need to recalculate locked or overridden grades
708 } else {
4c8893ed 709 if (!empty($dependson[$grade_grade->itemid])) {
0db54b5b 710 $dependencydepth[$grade_grade->itemid] = 1;
4c8893ed 711 $todo[] = $grade_grade->itemid;
712 }
6391ebe7 713 }
714 }
0db54b5b
DW
715
716 // Flatten the dependency tree and count number of branches to each leaf.
717 self::flatten_dependencies_array($dependson, $dependencydepth);
718
d297269d 719 if (!$hiddenfound) {
5232d3f2
DW
720 return array('unknown' => array(),
721 'altered' => array(),
722 'alteredgrademax' => array(),
53771c40
DW
723 'alteredgrademin' => array(),
724 'alteredaggregationstatus' => array(),
725 'alteredaggregationweight' => array());
d297269d 726 }
5a59aeb1
DW
727 // This line ensures that $dependencydepth has the same number of items as $todo.
728 $dependencydepth = array_intersect_key($dependencydepth, array_flip($todo));
0db54b5b
DW
729 // We need to resort the todo list by the dependency depth. This guarantees we process the leaves, then the branches.
730 array_multisort($dependencydepth, $todo);
6391ebe7 731
d297269d 732 $max = count($todo);
61541a5a 733 $hidden_precursors = null;
6391ebe7 734 for($i=0; $i<$max; $i++) {
735 $found = false;
736 foreach($todo as $key=>$do) {
61541a5a
AD
737 $hidden_precursors = array_intersect($dependson[$do], $unknown);
738 if ($hidden_precursors) {
6391ebe7 739 // this item depends on hidden grade indirectly
d297269d 740 $unknown[$do] = $do;
6391ebe7 741 unset($todo[$key]);
742 $found = true;
d297269d 743 continue;
744
745 } else if (!array_intersect($dependson[$do], $todo)) {
61541a5a
AD
746 $hidden_precursors = array_intersect($dependson[$do], array_keys($altered));
747 if (!$hidden_precursors) {
d297269d 748 // hiding does not affect this grade
749 unset($todo[$key]);
750 $found = true;
751 continue;
752
753 } else {
754 // depends on altered grades - we should try to recalculate if possible
61541a5a
AD
755 if ($grade_items[$do]->is_calculated() or
756 (!$grade_items[$do]->is_category_item() and !$grade_items[$do]->is_course_item())
757 ) {
0db54b5b
DW
758 // This is a grade item that is not a category or course and has been affected by grade hiding.
759 // I guess this means it is a calculation that needs to be recalculated.
d297269d 760 $unknown[$do] = $do;
761 unset($todo[$key]);
762 $found = true;
763 continue;
89a5f827 764
d297269d 765 } else {
0db54b5b 766 // This is a grade category (or course).
d297269d 767 $grade_category = $grade_items[$do]->load_item_category();
89a5f827 768
0db54b5b 769 // Build a new list of the grades in this category.
d297269d 770 $values = array();
0db54b5b
DW
771 $immediatedepends = $grade_items[$do]->depends_on();
772 foreach ($immediatedepends as $itemid) {
d297269d 773 if (array_key_exists($itemid, $altered)) {
61541a5a 774 //nulling an altered precursor
d297269d 775 $values[$itemid] = $altered[$itemid];
0db54b5b
DW
776 if (is_null($values[$itemid])) {
777 // This means this was a hidden grade item removed from the result.
778 unset($values[$itemid]);
779 }
61541a5a 780 } elseif (empty($values[$itemid])) {
d297269d 781 $values[$itemid] = $grade_grades[$itemid]->finalgrade;
782 }
783 }
89a5f827 784
78358cd6 785 foreach ($values as $itemid=>$value) {
786 if ($grade_grades[$itemid]->is_excluded()) {
787 unset($values[$itemid]);
53771c40 788 $alteredaggregationstatus[$itemid] = 'excluded';
6077a4d4 789 $alteredaggregationweight[$itemid] = null;
78358cd6 790 continue;
791 }
0db54b5b
DW
792 // The grade min/max may have been altered by hiding.
793 $grademin = $grade_items[$itemid]->grademin;
794 if (isset($alteredgrademin[$itemid])) {
795 $grademin = $alteredgrademin[$itemid];
796 }
797 $grademax = $grade_items[$itemid]->grademax;
798 if (isset($alteredgrademax[$itemid])) {
799 $grademax = $alteredgrademax[$itemid];
800 }
801 $values[$itemid] = grade_grade::standardise_score($value, $grademin, $grademax, 0, 1);
78358cd6 802 }
803
d297269d 804 if ($grade_category->aggregateonlygraded) {
805 foreach ($values as $itemid=>$value) {
806 if (is_null($value)) {
807 unset($values[$itemid]);
53771c40 808 $alteredaggregationstatus[$itemid] = 'novalue';
6077a4d4 809 $alteredaggregationweight[$itemid] = null;
d297269d 810 }
811 }
812 } else {
813 foreach ($values as $itemid=>$value) {
814 if (is_null($value)) {
78358cd6 815 $values[$itemid] = 0;
d297269d 816 }
817 }
818 }
d297269d 819
820 // limit and sort
53771c40 821 $allvalues = $values;
a9e38ac8 822 $grade_category->apply_limit_rules($values, $grade_items);
53771c40
DW
823
824 $moredropped = array_diff($allvalues, $values);
825 foreach ($moredropped as $drop => $unused) {
826 $alteredaggregationstatus[$drop] = 'dropped';
6077a4d4 827 $alteredaggregationweight[$drop] = null;
53771c40
DW
828 }
829
830 foreach ($values as $itemid => $val) {
831 if ($grade_category->is_extracredit_used() && ($grade_items[$itemid]->aggregationcoef > 0)) {
832 $alteredaggregationstatus[$itemid] = 'extra';
833 }
834 }
835
d297269d 836 asort($values, SORT_NUMERIC);
89a5f827 837
d297269d 838 // let's see we have still enough grades to do any statistics
839 if (count($values) == 0) {
840 // not enough attempts yet
841 $altered[$do] = null;
842 unset($todo[$key]);
843 $found = true;
844 continue;
845 }
846
53771c40
DW
847 $usedweights = array();
848 $adjustedgrade = $grade_category->aggregate_values_and_adjust_bounds($values, $grade_items, $usedweights);
d297269d 849
850 // recalculate the rawgrade back to requested range
5232d3f2
DW
851 $finalgrade = grade_grade::standardise_score($adjustedgrade['grade'],
852 0,
853 1,
854 $adjustedgrade['grademin'],
855 $adjustedgrade['grademax']);
89a5f827 856
53771c40
DW
857 foreach ($usedweights as $itemid => $weight) {
858 if (!isset($alteredaggregationstatus[$itemid])) {
859 $alteredaggregationstatus[$itemid] = 'used';
860 }
861 $alteredaggregationweight[$itemid] = $weight;
862 }
863
653a8648 864 $finalgrade = $grade_items[$do]->bounded_grade($finalgrade);
5232d3f2
DW
865 $alteredgrademin[$do] = $adjustedgrade['grademin'];
866 $alteredgrademax[$do] = $adjustedgrade['grademax'];
0db54b5b
DW
867 // We need to muck with the "in-memory" grade_items records so
868 // that subsequent calculations will use the adjusted grademin and grademax.
869 $grade_items[$do]->grademin = $adjustedgrade['grademin'];
870 $grade_items[$do]->grademax = $adjustedgrade['grademax'];
d297269d 871
872 $altered[$do] = $finalgrade;
873 unset($todo[$key]);
874 $found = true;
875 continue;
876 }
877 }
6391ebe7 878 }
879 }
880 if (!$found) {
881 break;
882 }
883 }
884
5232d3f2
DW
885 return array('unknown' => $unknown,
886 'altered' => $altered,
887 'alteredgrademax' => $alteredgrademax,
53771c40
DW
888 'alteredgrademin' => $alteredgrademin,
889 'alteredaggregationstatus' => $alteredaggregationstatus,
890 'alteredaggregationweight' => $alteredaggregationweight);
6391ebe7 891 }
66b61ac6 892
893 /**
894 * Returns true if the grade's value is superior or equal to the grade item's gradepass value, false otherwise.
a153c9f2
AD
895 *
896 * @param grade_item $grade_item An optional grade_item of which gradepass value we can use, saves having to load the grade_grade's grade_item
897 * @return bool
66b61ac6 898 */
da3801e8 899 public function is_passed($grade_item = null) {
66b61ac6 900 if (empty($grade_item)) {
901 if (!isset($this->grade_item)) {
902 $this->load_grade_item();
903 }
904 } else {
905 $this->grade_item = $grade_item;
906 $this->itemid = $grade_item->id;
907 }
e6477988 908
909 // Return null if finalgrade is null
910 if (is_null($this->finalgrade)) {
911 return null;
912 }
913
914 // Return null if gradepass == grademin or gradepass is null
915 if (is_null($this->grade_item->gradepass) || $this->grade_item->gradepass == $this->grade_item->grademin) {
916 return null;
917 }
918
66b61ac6 919 return $this->finalgrade >= $this->grade_item->gradepass;
920 }
43ea3f3c 921
a153c9f2
AD
922 /**
923 * Insert the grade_grade instance into the database.
924 *
925 * @param string $source From where was the object inserted (mod/forum, manual, etc.)
926 * @return int The new grade_grade ID if successful, false otherwise
927 */
da3801e8 928 public function insert($source=null) {
a153c9f2 929 // TODO: dategraded hack - do not update times, they are used for submission and grading (MDL-31379)
da3801e8 930 //$this->timecreated = $this->timemodified = time();
43ea3f3c 931 return parent::insert($source);
932 }
25bcd908 933
934 /**
935 * In addition to update() as defined in grade_object rounds the float numbers using php function,
936 * the reason is we need to compare the db value with computed number to skip updates if possible.
a153c9f2 937 *
25bcd908 938 * @param string $source from where was the object inserted (mod/forum, manual, etc.)
a153c9f2 939 * @return bool success
25bcd908 940 */
da3801e8 941 public function update($source=null) {
25bcd908 942 $this->rawgrade = grade_floatval($this->rawgrade);
943 $this->finalgrade = grade_floatval($this->finalgrade);
944 $this->rawgrademin = grade_floatval($this->rawgrademin);
945 $this->rawgrademax = grade_floatval($this->rawgrademax);
946 return parent::update($source);
947 }
cb551e36 948
2e0b3490
MN
949 /**
950 * Deletes the grade_grade instance from the database.
951 *
952 * @param string $source The location the deletion occurred (mod/forum, manual, etc.).
953 * @return bool Returns true if the deletion was successful, false otherwise.
954 */
955 public function delete($source = null) {
956 $success = parent::delete($source);
957
958 // If the grade was deleted successfully trigger a grade_deleted event.
959 if ($success) {
960 $this->load_grade_item();
961 \core\event\grade_deleted::create_from_grade($this)->trigger();
962 }
963
964 return $success;
965 }
966
4e781c7b 967 /**
968 * Used to notify the completion system (if necessary) that a user's grade
82bd6a5e 969 * has changed, and clear up a possible score cache.
a153c9f2
AD
970 *
971 * @param bool $deleted True if grade was actually deleted
4e781c7b 972 */
e01efa2c 973 protected function notify_changed($deleted) {
9b5e2461 974 global $CFG;
82bd6a5e 975
e01efa2c 976 // Condition code may cache the grades for conditional availability of
977 // modules or sections. (This code should use a hook for communication
978 // with plugin, but hooks are not implemented at time of writing.)
979 if (!empty($CFG->enableavailability) && class_exists('\availability_grade\callbacks')) {
980 \availability_grade\callbacks::grade_changed($this->userid);
b85b9b7c
MG
981 }
982
4e781c7b 983 require_once($CFG->libdir.'/completionlib.php');
cb551e36 984
49f6e5f4 985 // Bail out immediately if completion is not enabled for site (saves loading
ddf368a2 986 // grade item & requiring the restore stuff).
cb551e36 987 if (!completion_info::is_enabled_for_site()) {
49f6e5f4 988 return;
989 }
cb551e36 990
ddf368a2 991 // Ignore during restore, as completion data will be updated anyway and
992 // doing it now will result in incorrect dates (it will say they got the
993 // grade completion now, instead of the correct time).
994 if (class_exists('restore_controller', false) && restore_controller::is_executing()) {
995 return;
996 }
997
49f6e5f4 998 // Load information about grade item
999 $this->load_grade_item();
cb551e36 1000
49f6e5f4 1001 // Only course-modules have completion data
cb551e36 1002 if ($this->grade_item->itemtype!='mod') {
49f6e5f4 1003 return;
1004 }
cb551e36 1005
4e781c7b 1006 // Use $COURSE if available otherwise get it via item fields
9b5e2461 1007 $course = get_course($this->grade_item->courseid, false);
4e781c7b 1008
49f6e5f4 1009 // Bail out if completion is not enabled for course
cb551e36 1010 $completion = new completion_info($course);
1011 if (!$completion->is_enabled()) {
4e781c7b 1012 return;
1013 }
1014
49f6e5f4 1015 // Get course-module
cb551e36 1016 $cm = get_coursemodule_from_instance($this->grade_item->itemmodule,
1017 $this->grade_item->iteminstance, $this->grade_item->courseid);
ad275cb4 1018 // If the course-module doesn't exist, display a warning...
cb551e36 1019 if (!$cm) {
ad275cb4 1020 // ...unless the grade is being deleted in which case it's likely
1021 // that the course-module was just deleted too, so that's okay.
1022 if (!$deleted) {
1023 debugging("Couldn't find course-module for module '" .
1024 $this->grade_item->itemmodule . "', instance '" .
1025 $this->grade_item->iteminstance . "', course '" .
1026 $this->grade_item->courseid . "'");
1027 }
4e781c7b 1028 return;
1029 }
1030
1031 // Pass information on to completion system
cb551e36 1032 $completion->inform_grade_changed($cm, $this->grade_item, $this, $deleted);
bfe969e8
DW
1033 }
1034
1035 /**
1036 * Get some useful information about how this grade_grade is reflected in the aggregation
1037 * for the grade_category. For example this could be an extra credit item, and it could be
1038 * dropped because it's in the X lowest or highest.
1039 *
53771c40 1040 * @return array(status, weight) - A keyword and a numerical weight that represents how this grade was included in the aggregation.
bfe969e8 1041 */
6077a4d4 1042 function get_aggregation_hint() {
53771c40 1043 return array('status' => $this->get_aggregationstatus(),
6077a4d4 1044 'weight' => $this->get_aggregationweight());
bfe969e8 1045 }
e5c674f1 1046}