MDL-54613 unit tests: Add iteminstance to test grade_item
[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
c07775df
EM
340 /**
341 * Returns the minimum and maximum number of points this grade is graded with respect to.
342 *
ebea19cb
FM
343 * @since Moodle 2.8.7, 2.9.1
344 * @return array A list containing, in order, the minimum and maximum number of points.
c07775df 345 */
ebea19cb
FM
346 protected function get_grade_min_and_max() {
347 global $CFG;
c07775df
EM
348 $this->load_grade_item();
349
ebea19cb
FM
350 // When the following setting is turned on we use the grade_grade raw min and max values.
351 $minmaxtouse = grade_get_setting($this->grade_item->courseid, 'minmaxtouse', $CFG->grade_minmaxtouse);
c07775df 352
4d4dcc27
AG
353 // Check to see if the gradebook is frozen. This allows grades to not be altered at all until a user verifies that they
354 // wish to update the grades.
355 $gradebookcalculationsfreeze = get_config('core', 'gradebook_calculations_freeze_' . $this->grade_item->courseid);
356 // Gradebook is frozen, run through old code.
357 if ($gradebookcalculationsfreeze && (int)$gradebookcalculationsfreeze <= 20150627) {
358 // Only aggregate items use separate min grades.
359 if ($minmaxtouse == GRADE_MIN_MAX_FROM_GRADE_GRADE || $this->grade_item->is_aggregate_item()) {
360 return array($this->rawgrademin, $this->rawgrademax);
361 } else {
362 return array($this->grade_item->grademin, $this->grade_item->grademax);
363 }
c07775df 364 } else {
4d4dcc27
AG
365 // Only aggregate items use separate min grades, unless they are calculated grade items.
366 if (($this->grade_item->is_aggregate_item() && !$this->grade_item->is_calculated())
367 || $minmaxtouse == GRADE_MIN_MAX_FROM_GRADE_GRADE) {
368 return array($this->rawgrademin, $this->rawgrademax);
369 } else {
370 return array($this->grade_item->grademin, $this->grade_item->grademax);
371 }
c07775df
EM
372 }
373 }
374
375 /**
376 * Returns the minimum number of points this grade is graded with.
377 *
ebea19cb 378 * @since Moodle 2.8.7, 2.9.1
c07775df
EM
379 * @return float The minimum number of points
380 */
381 public function get_grade_min() {
ebea19cb 382 list($min, $max) = $this->get_grade_min_and_max();
c07775df
EM
383
384 return $min;
385 }
386
387 /**
388 * Returns the maximum number of points this grade is graded with respect to.
389 *
ebea19cb 390 * @since Moodle 2.8.7, 2.9.1
c07775df
EM
391 * @return float The maximum number of points
392 */
393 public function get_grade_max() {
ebea19cb 394 list($min, $max) = $this->get_grade_min_and_max();
c07775df
EM
395
396 return $max;
397 }
398
ced5ee59 399 /**
a153c9f2
AD
400 * Returns timestamp when last graded, null if no grade present
401 *
ced5ee59 402 * @return int
403 */
da3801e8 404 public function get_dategraded() {
a153c9f2 405 //TODO: HACK - create new fields (MDL-31379)
cf12f6c5 406 if (is_null($this->finalgrade) and is_null($this->feedback)) {
ced5ee59 407 return null; // no grade == no date
408 } else if ($this->overridden) {
409 return $this->overridden;
410 } else {
411 return $this->timemodified;
412 }
413 }
414
23207a1a 415 /**
416 * Set the overridden status of grade
a153c9f2
AD
417 *
418 * @param bool $state requested overridden state
419 * @param bool $refresh refresh grades from external activities if needed
420 * @return bool true is db state changed
23207a1a 421 */
da3801e8 422 public function set_overridden($state, $refresh = true) {
23207a1a 423 if (empty($this->overridden) and $state) {
424 $this->overridden = time();
425 $this->update();
426 return true;
427
428 } else if (!empty($this->overridden) and !$state) {
429 $this->overridden = 0;
430 $this->update();
0f392ff4 431
432 if ($refresh) {
433 //refresh when unlocking
434 $this->grade_item->refresh_grades($this->userid);
435 }
436
23207a1a 437 return true;
438 }
439 return false;
440 }
441
442 /**
443 * Checks if grade excluded from aggregation functions
a153c9f2
AD
444 *
445 * @return bool True if grade is excluded from aggregation
23207a1a 446 */
da3801e8 447 public function is_excluded() {
23207a1a 448 return !empty($this->excluded);
449 }
450
451 /**
452 * Set the excluded status of grade
a153c9f2
AD
453 *
454 * @param bool $state requested excluded state
455 * @return bool True is database state changed
23207a1a 456 */
da3801e8 457 public function set_excluded($state) {
23207a1a 458 if (empty($this->excluded) and $state) {
459 $this->excluded = time();
460 $this->update();
461 return true;
462
463 } else if (!empty($this->excluded) and !$state) {
464 $this->excluded = 0;
465 $this->update();
466 return true;
467 }
468 return false;
469 }
470
2cc4b0f9 471 /**
388234f4 472 * Lock/unlock this grade.
2cc4b0f9 473 *
a153c9f2
AD
474 * @param int $lockedstate 0, 1 or a timestamp int(10) after which date the item will be locked.
475 * @param bool $cascade Ignored param
476 * @param bool $refresh Refresh grades when unlocking
477 * @return bool True if successful, false if can not set new lock state for grade
2cc4b0f9 478 */
da3801e8 479 public function set_locked($lockedstate, $cascade=false, $refresh=true) {
2cc4b0f9 480 $this->load_grade_item();
481
482 if ($lockedstate) {
2cc4b0f9 483 if ($this->grade_item->needsupdate) {
484 //can not lock grade if final not calculated!
485 return false;
486 }
487
488 $this->locked = time();
489 $this->update();
490
491 return true;
492
493 } else {
fb0e3570 494 if (!empty($this->locked) and $this->locktime < time()) {
495 //we have to reset locktime or else it would lock up again
496 $this->locktime = 0;
2cc4b0f9 497 }
498
499 // remove the locked flag
500 $this->locked = 0;
2cc4b0f9 501 $this->update();
502
25bcd908 503 if ($refresh and !$this->is_overridden()) {
504 //refresh when unlocking and not overridden
2b0f65e2 505 $this->grade_item->refresh_grades($this->userid);
506 }
507
2cc4b0f9 508 return true;
509 }
510 }
511
fb0e3570 512 /**
a153c9f2
AD
513 * Lock the grade if needed. Make sure this is called only when final grades are valid
514 *
ddc20982 515 * @param array $items array of all grade item ids
fb0e3570 516 * @return void
517 */
f20edd52 518 public static function check_locktime_all($items) {
da3801e8 519 global $CFG, $DB;
fb0e3570 520
fb0e3570 521 $now = time(); // no rounding needed, this is not supposed to be called every 10 seconds
5b0af8c5 522 list($usql, $params) = $DB->get_in_or_equal($items);
523 $params[] = $now;
1b42e677
EL
524 $rs = $DB->get_recordset_select('grade_grades', "itemid $usql AND locked = 0 AND locktime > 0 AND locktime < ?", $params);
525 foreach ($rs as $grade) {
526 $grade_grade = new grade_grade($grade, false);
527 $grade_grade->locked = time();
528 $grade_grade->update('locktime');
fb0e3570 529 }
1b42e677 530 $rs->close();
fb0e3570 531 }
532
533 /**
534 * Set the locktime for this grade.
535 *
536 * @param int $locktime timestamp for lock to activate
537 * @return void
538 */
da3801e8 539 public function set_locktime($locktime) {
fb0e3570 540 $this->locktime = $locktime;
541 $this->update();
542 }
7e3c9767 543
fb0e3570 544 /**
a153c9f2 545 * Get the locktime for this grade.
fb0e3570 546 *
547 * @return int $locktime timestamp for lock to activate
548 */
da3801e8 549 public function get_locktime() {
fb0e3570 550 $this->load_grade_item();
7e3c9767 551
fb0e3570 552 $item_locktime = $this->grade_item->get_locktime();
7e3c9767 553
fb0e3570 554 if (empty($this->locktime) or ($item_locktime and $item_locktime < $this->locktime)) {
555 return $item_locktime;
556
557 } else {
558 return $this->locktime;
7e3c9767 559 }
560 }
561
22e23c78 562 /**
f60c61b1 563 * Check grade hidden status. Uses data from both grade item and grade.
a153c9f2
AD
564 *
565 * @return bool true if hidden, false if not
22e23c78 566 */
da3801e8 567 public function is_hidden() {
f60c61b1 568 $this->load_grade_item();
1f0e4921 569 if (empty($this->grade_item)) {
570 return $this->hidden == 1 or ($this->hidden != 0 and $this->hidden > time());
571 } else {
572 return $this->hidden == 1 or ($this->hidden != 0 and $this->hidden > time()) or $this->grade_item->is_hidden();
da3801e8 573 }
f60c61b1 574 }
575
597f50e6 576 /**
577 * Check grade hidden status. Uses data from both grade item and grade.
a153c9f2
AD
578 *
579 * @return bool true if hiddenuntil, false if not
597f50e6 580 */
da3801e8 581 public function is_hiddenuntil() {
597f50e6 582 $this->load_grade_item();
583
584 if ($this->hidden == 1 or $this->grade_item->hidden == 1) {
585 return false; //always hidden
586 }
587
588 if ($this->hidden > 1 or $this->grade_item->hidden > 1) {
589 return true;
590 }
591
592 return false;
593 }
594
f60c61b1 595 /**
596 * Check grade hidden status. Uses data from both grade item and grade.
a153c9f2 597 *
f60c61b1 598 * @return int 0 means visible, 1 hidden always, timestamp hidden until
599 */
da3801e8 600 public function get_hidden() {
f60c61b1 601 $this->load_grade_item();
602
603 $item_hidden = $this->grade_item->get_hidden();
604
605 if ($item_hidden == 1) {
606 return 1;
22e23c78 607
f60c61b1 608 } else if ($item_hidden == 0) {
609 return $this->hidden;
610
611 } else {
612 if ($this->hidden == 0) {
613 return $item_hidden;
614 } else if ($this->hidden == 1) {
615 return 1;
616 } else if ($this->hidden > $item_hidden) {
617 return $this->hidden;
618 } else {
619 return $item_hidden;
620 }
621 }
22e23c78 622 }
623
624 /**
625 * Set the hidden status of grade, 0 mean visible, 1 always hidden, number means date to hide until.
a153c9f2 626 *
22e23c78 627 * @param int $hidden new hidden status
a153c9f2 628 * @param bool $cascade ignored
22e23c78 629 */
da3801e8 630 public function set_hidden($hidden, $cascade=false) {
22e23c78 631 $this->hidden = $hidden;
632 $this->update();
633 }
634
e5c674f1 635 /**
3ee5c201 636 * Finds and returns a grade_grade instance based on params.
61c33818 637 *
f92dcad8 638 * @param array $params associative arrays varname=>value
a153c9f2 639 * @return grade_grade Returns a grade_grade instance or false if none found
f92dcad8 640 */
da3801e8 641 public static function fetch($params) {
f3ac8eb4 642 return grade_object::fetch_helper('grade_grades', 'grade_grade', $params);
f92dcad8 643 }
61c33818 644
f92dcad8 645 /**
3ee5c201 646 * Finds and returns all grade_grade instances based on params.
f92dcad8 647 *
648 * @param array $params associative arrays varname=>value
f7d515b6 649 * @return array array of grade_grade instances or false if none found.
f92dcad8 650 */
da3801e8 651 public static function fetch_all($params) {
f3ac8eb4 652 return grade_object::fetch_all_helper('grade_grades', 'grade_grade', $params);
4cf1b9be 653 }
654
a8995b34 655 /**
ac9b0805 656 * Given a float value situated between a source minimum and a source maximum, converts it to the
657 * corresponding value situated between a target minimum and a target maximum. Thanks to Darlene
658 * for the formula :-)
a8995b34 659 *
ac9b0805 660 * @param float $rawgrade
661 * @param float $source_min
662 * @param float $source_max
663 * @param float $target_min
664 * @param float $target_max
665 * @return float Converted value
8f4a626d 666 */
da3801e8 667 public static function standardise_score($rawgrade, $source_min, $source_max, $target_min, $target_max) {
9580a21f 668 if (is_null($rawgrade)) {
ba74762b 669 return null;
9580a21f 670 }
671
2e0d37fe 672 if ($source_max == $source_min or $target_min == $target_max) {
673 // prevent division by 0
674 return $target_max;
675 }
676
ac9b0805 677 $factor = ($rawgrade - $source_min) / ($source_max - $source_min);
678 $diff = $target_max - $target_min;
679 $standardised_value = $factor * $diff + $target_min;
680 return $standardised_value;
6c76ea8d 681 }
6391ebe7 682
0db54b5b
DW
683 /**
684 * Given an array like this:
685 * $a = array(1=>array(2, 3),
686 * 2=>array(4),
687 * 3=>array(1),
688 * 4=>array())
689 * this function fully resolves the dependencies so each value will be an array of
690 * the all items this item depends on and their dependencies (and their dependencies...).
691 * It should not explode if there are circular dependencies.
692 * The dependency depth array will list the number of branches in the tree above each leaf.
693 *
694 * @param array $dependson Array to flatten
695 * @param array $dependencydepth Array of itemids => depth. Initially these should be all set to 1.
696 * @return array Flattened array
697 */
698 protected static function flatten_dependencies_array(&$dependson, &$dependencydepth) {
699 // Flatten the nested dependencies - this will handle recursion bombs because it removes duplicates.
700 $somethingchanged = true;
701 while ($somethingchanged) {
702 $somethingchanged = false;
703
704 foreach ($dependson as $itemid => $depends) {
705 // Make a copy so we can tell if it changed.
706 $before = $dependson[$itemid];
707 foreach ($depends as $subitemid => $subdepends) {
708 $dependson[$itemid] = array_unique(array_merge($depends, $dependson[$subdepends]));
709 sort($dependson[$itemid], SORT_NUMERIC);
710 }
711 if ($before != $dependson[$itemid]) {
712 $somethingchanged = true;
713 if (!isset($dependencydepth[$itemid])) {
714 $dependencydepth[$itemid] = 1;
715 } else {
716 $dependencydepth[$itemid]++;
717 }
718 }
719 }
720 }
721 }
722
6391ebe7 723 /**
724 * Return array of grade item ids that are either hidden or indirectly depend
725 * on hidden grades, excluded grades are not returned.
d297269d 726 * THIS IS A REALLY BIG HACK! to be replaced by conditional aggregation of hidden grades in 2.0
727 *
a153c9f2
AD
728 * @param array $grade_grades all course grades of one user, & used for better internal caching
729 * @param array $grade_items array of grade items, & used for better internal caching
5232d3f2
DW
730 * @return array This is an array of 3 arrays:
731 * unknown => list of item ids that may be affected by hiding (with the calculated grade as the value)
732 * altered => list of item ids that are definitely affected by hiding (with the calculated grade as the value)
733 * alteredgrademax => for each item in altered or unknown, the new value of the grademax
6070e533
DW
734 * alteredgrademin => for each item in altered or unknown, the new value of the grademin
735 * alteredgradestatus => for each item with a modified status - the value of the new status
736 * alteredgradeweight => for each item with a modified weight - the value of the new weight
6391ebe7 737 */
da3801e8 738 public static function get_hiding_affected(&$grade_grades, &$grade_items) {
d297269d 739 global $CFG;
740
6391ebe7 741 if (count($grade_grades) !== count($grade_items)) {
2f137aa1 742 print_error('invalidarraysize', 'debug', '', 'grade_grade::get_hiding_affected()!');
6391ebe7 743 }
744
745 $dependson = array();
d297269d 746 $todo = array();
747 $unknown = array(); // can not find altered
748 $altered = array(); // altered grades
5232d3f2
DW
749 $alteredgrademax = array(); // Altered grade max values.
750 $alteredgrademin = array(); // Altered grade min values.
53771c40
DW
751 $alteredaggregationstatus = array(); // Altered aggregation status.
752 $alteredaggregationweight = array(); // Altered aggregation weight.
0db54b5b 753 $dependencydepth = array();
d297269d 754
d297269d 755 $hiddenfound = false;
4c8893ed 756 foreach($grade_grades as $itemid=>$unused) {
757 $grade_grade =& $grade_grades[$itemid];
0db54b5b
DW
758 // We need the immediate dependencies of all every grade_item so we can calculate nested dependencies.
759 $dependson[$grade_grade->itemid] = $grade_items[$grade_grade->itemid]->depends_on();
d297269d 760 if ($grade_grade->is_excluded()) {
761 //nothing to do, aggregation is ok
762 } else if ($grade_grade->is_hidden()) {
763 $hiddenfound = true;
ced5ee59 764 $altered[$grade_grade->itemid] = null;
edffadfd
DW
765 $alteredaggregationstatus[$grade_grade->itemid] = 'dropped';
766 $alteredaggregationweight[$grade_grade->itemid] = 0;
4c8893ed 767 } else if ($grade_grade->is_locked() or $grade_grade->is_overridden()) {
768 // no need to recalculate locked or overridden grades
769 } else {
4c8893ed 770 if (!empty($dependson[$grade_grade->itemid])) {
0db54b5b 771 $dependencydepth[$grade_grade->itemid] = 1;
4c8893ed 772 $todo[] = $grade_grade->itemid;
773 }
6391ebe7 774 }
775 }
0db54b5b
DW
776
777 // Flatten the dependency tree and count number of branches to each leaf.
778 self::flatten_dependencies_array($dependson, $dependencydepth);
779
d297269d 780 if (!$hiddenfound) {
5232d3f2
DW
781 return array('unknown' => array(),
782 'altered' => array(),
783 'alteredgrademax' => array(),
53771c40
DW
784 'alteredgrademin' => array(),
785 'alteredaggregationstatus' => array(),
786 'alteredaggregationweight' => array());
d297269d 787 }
5a59aeb1
DW
788 // This line ensures that $dependencydepth has the same number of items as $todo.
789 $dependencydepth = array_intersect_key($dependencydepth, array_flip($todo));
0db54b5b
DW
790 // We need to resort the todo list by the dependency depth. This guarantees we process the leaves, then the branches.
791 array_multisort($dependencydepth, $todo);
6391ebe7 792
d297269d 793 $max = count($todo);
61541a5a 794 $hidden_precursors = null;
6391ebe7 795 for($i=0; $i<$max; $i++) {
796 $found = false;
797 foreach($todo as $key=>$do) {
61541a5a
AD
798 $hidden_precursors = array_intersect($dependson[$do], $unknown);
799 if ($hidden_precursors) {
6391ebe7 800 // this item depends on hidden grade indirectly
d297269d 801 $unknown[$do] = $do;
6391ebe7 802 unset($todo[$key]);
803 $found = true;
d297269d 804 continue;
805
806 } else if (!array_intersect($dependson[$do], $todo)) {
61541a5a 807 $hidden_precursors = array_intersect($dependson[$do], array_keys($altered));
f3460b0f
PM
808 // If the dependency is a sum aggregation, we need to process it as if it had hidden items.
809 // The reason for this, is that the code will recalculate the maxgrade by removing ungraded
810 // items and accounting for 'drop x grades' and then stored back in our virtual grade_items.
811 // This recalculation is necessary because there will be a call to:
812 // $grade_category->aggregate_values_and_adjust_bounds
813 // for the top level grade that will depend on knowing what that caclulated grademax is
814 // and it finds that value by checking the virtual grade_items.
815 $issumaggregate = false;
816 if ($grade_items[$do]->itemtype == 'category') {
817 $issumaggregate = $grade_items[$do]->load_item_category()->aggregation == GRADE_AGGREGATE_SUM;
818 }
819 if (!$hidden_precursors && !$issumaggregate) {
d297269d 820 unset($todo[$key]);
821 $found = true;
822 continue;
823
824 } else {
825 // depends on altered grades - we should try to recalculate if possible
61541a5a
AD
826 if ($grade_items[$do]->is_calculated() or
827 (!$grade_items[$do]->is_category_item() and !$grade_items[$do]->is_course_item())
828 ) {
0db54b5b
DW
829 // This is a grade item that is not a category or course and has been affected by grade hiding.
830 // I guess this means it is a calculation that needs to be recalculated.
d297269d 831 $unknown[$do] = $do;
832 unset($todo[$key]);
833 $found = true;
834 continue;
89a5f827 835
d297269d 836 } else {
0db54b5b 837 // This is a grade category (or course).
d297269d 838 $grade_category = $grade_items[$do]->load_item_category();
89a5f827 839
0db54b5b 840 // Build a new list of the grades in this category.
d297269d 841 $values = array();
0db54b5b
DW
842 $immediatedepends = $grade_items[$do]->depends_on();
843 foreach ($immediatedepends as $itemid) {
d297269d 844 if (array_key_exists($itemid, $altered)) {
61541a5a 845 //nulling an altered precursor
d297269d 846 $values[$itemid] = $altered[$itemid];
0db54b5b
DW
847 if (is_null($values[$itemid])) {
848 // This means this was a hidden grade item removed from the result.
849 unset($values[$itemid]);
850 }
61541a5a 851 } elseif (empty($values[$itemid])) {
d297269d 852 $values[$itemid] = $grade_grades[$itemid]->finalgrade;
853 }
854 }
89a5f827 855
78358cd6 856 foreach ($values as $itemid=>$value) {
857 if ($grade_grades[$itemid]->is_excluded()) {
858 unset($values[$itemid]);
53771c40 859 $alteredaggregationstatus[$itemid] = 'excluded';
6077a4d4 860 $alteredaggregationweight[$itemid] = null;
78358cd6 861 continue;
862 }
0db54b5b
DW
863 // The grade min/max may have been altered by hiding.
864 $grademin = $grade_items[$itemid]->grademin;
865 if (isset($alteredgrademin[$itemid])) {
866 $grademin = $alteredgrademin[$itemid];
867 }
868 $grademax = $grade_items[$itemid]->grademax;
869 if (isset($alteredgrademax[$itemid])) {
870 $grademax = $alteredgrademax[$itemid];
871 }
872 $values[$itemid] = grade_grade::standardise_score($value, $grademin, $grademax, 0, 1);
78358cd6 873 }
874
d297269d 875 if ($grade_category->aggregateonlygraded) {
876 foreach ($values as $itemid=>$value) {
877 if (is_null($value)) {
878 unset($values[$itemid]);
53771c40 879 $alteredaggregationstatus[$itemid] = 'novalue';
6077a4d4 880 $alteredaggregationweight[$itemid] = null;
d297269d 881 }
882 }
883 } else {
884 foreach ($values as $itemid=>$value) {
885 if (is_null($value)) {
78358cd6 886 $values[$itemid] = 0;
d297269d 887 }
888 }
889 }
d297269d 890
891 // limit and sort
53771c40 892 $allvalues = $values;
a9e38ac8 893 $grade_category->apply_limit_rules($values, $grade_items);
53771c40
DW
894
895 $moredropped = array_diff($allvalues, $values);
896 foreach ($moredropped as $drop => $unused) {
897 $alteredaggregationstatus[$drop] = 'dropped';
6077a4d4 898 $alteredaggregationweight[$drop] = null;
53771c40
DW
899 }
900
901 foreach ($values as $itemid => $val) {
902 if ($grade_category->is_extracredit_used() && ($grade_items[$itemid]->aggregationcoef > 0)) {
903 $alteredaggregationstatus[$itemid] = 'extra';
904 }
905 }
906
d297269d 907 asort($values, SORT_NUMERIC);
89a5f827 908
d297269d 909 // let's see we have still enough grades to do any statistics
910 if (count($values) == 0) {
911 // not enough attempts yet
912 $altered[$do] = null;
913 unset($todo[$key]);
914 $found = true;
915 continue;
916 }
917
53771c40
DW
918 $usedweights = array();
919 $adjustedgrade = $grade_category->aggregate_values_and_adjust_bounds($values, $grade_items, $usedweights);
d297269d 920
921 // recalculate the rawgrade back to requested range
5232d3f2
DW
922 $finalgrade = grade_grade::standardise_score($adjustedgrade['grade'],
923 0,
924 1,
925 $adjustedgrade['grademin'],
926 $adjustedgrade['grademax']);
89a5f827 927
53771c40
DW
928 foreach ($usedweights as $itemid => $weight) {
929 if (!isset($alteredaggregationstatus[$itemid])) {
930 $alteredaggregationstatus[$itemid] = 'used';
931 }
932 $alteredaggregationweight[$itemid] = $weight;
933 }
934
653a8648 935 $finalgrade = $grade_items[$do]->bounded_grade($finalgrade);
5232d3f2
DW
936 $alteredgrademin[$do] = $adjustedgrade['grademin'];
937 $alteredgrademax[$do] = $adjustedgrade['grademax'];
0db54b5b
DW
938 // We need to muck with the "in-memory" grade_items records so
939 // that subsequent calculations will use the adjusted grademin and grademax.
940 $grade_items[$do]->grademin = $adjustedgrade['grademin'];
941 $grade_items[$do]->grademax = $adjustedgrade['grademax'];
d297269d 942
943 $altered[$do] = $finalgrade;
944 unset($todo[$key]);
945 $found = true;
946 continue;
947 }
948 }
6391ebe7 949 }
950 }
951 if (!$found) {
952 break;
953 }
954 }
955
5232d3f2
DW
956 return array('unknown' => $unknown,
957 'altered' => $altered,
958 'alteredgrademax' => $alteredgrademax,
53771c40
DW
959 'alteredgrademin' => $alteredgrademin,
960 'alteredaggregationstatus' => $alteredaggregationstatus,
961 'alteredaggregationweight' => $alteredaggregationweight);
6391ebe7 962 }
66b61ac6 963
964 /**
965 * Returns true if the grade's value is superior or equal to the grade item's gradepass value, false otherwise.
a153c9f2
AD
966 *
967 * @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
968 * @return bool
66b61ac6 969 */
da3801e8 970 public function is_passed($grade_item = null) {
66b61ac6 971 if (empty($grade_item)) {
972 if (!isset($this->grade_item)) {
973 $this->load_grade_item();
974 }
975 } else {
976 $this->grade_item = $grade_item;
977 $this->itemid = $grade_item->id;
978 }
e6477988 979
980 // Return null if finalgrade is null
981 if (is_null($this->finalgrade)) {
982 return null;
983 }
984
fdeda649
EM
985 // Return null if gradepass == grademin, gradepass is null, or grade item is a scale and gradepass is 0.
986 if (is_null($this->grade_item->gradepass)) {
987 return null;
988 } else if ($this->grade_item->gradepass == $this->grade_item->grademin) {
989 return null;
990 } else if ($this->grade_item->gradetype == GRADE_TYPE_SCALE && !grade_floats_different($this->grade_item->gradepass, 0.0)) {
e6477988 991 return null;
992 }
993
66b61ac6 994 return $this->finalgrade >= $this->grade_item->gradepass;
995 }
43ea3f3c 996
a153c9f2
AD
997 /**
998 * Insert the grade_grade instance into the database.
999 *
1000 * @param string $source From where was the object inserted (mod/forum, manual, etc.)
1001 * @return int The new grade_grade ID if successful, false otherwise
1002 */
da3801e8 1003 public function insert($source=null) {
a153c9f2 1004 // TODO: dategraded hack - do not update times, they are used for submission and grading (MDL-31379)
da3801e8 1005 //$this->timecreated = $this->timemodified = time();
43ea3f3c 1006 return parent::insert($source);
1007 }
25bcd908 1008
1009 /**
1010 * In addition to update() as defined in grade_object rounds the float numbers using php function,
1011 * the reason is we need to compare the db value with computed number to skip updates if possible.
a153c9f2 1012 *
25bcd908 1013 * @param string $source from where was the object inserted (mod/forum, manual, etc.)
a153c9f2 1014 * @return bool success
25bcd908 1015 */
da3801e8 1016 public function update($source=null) {
25bcd908 1017 $this->rawgrade = grade_floatval($this->rawgrade);
1018 $this->finalgrade = grade_floatval($this->finalgrade);
1019 $this->rawgrademin = grade_floatval($this->rawgrademin);
1020 $this->rawgrademax = grade_floatval($this->rawgrademax);
1021 return parent::update($source);
1022 }
cb551e36 1023
2e0b3490
MN
1024 /**
1025 * Deletes the grade_grade instance from the database.
1026 *
1027 * @param string $source The location the deletion occurred (mod/forum, manual, etc.).
1028 * @return bool Returns true if the deletion was successful, false otherwise.
1029 */
1030 public function delete($source = null) {
1031 $success = parent::delete($source);
1032
1033 // If the grade was deleted successfully trigger a grade_deleted event.
1034 if ($success) {
1035 $this->load_grade_item();
1036 \core\event\grade_deleted::create_from_grade($this)->trigger();
1037 }
1038
1039 return $success;
1040 }
1041
4e781c7b 1042 /**
1043 * Used to notify the completion system (if necessary) that a user's grade
82bd6a5e 1044 * has changed, and clear up a possible score cache.
a153c9f2
AD
1045 *
1046 * @param bool $deleted True if grade was actually deleted
4e781c7b 1047 */
e01efa2c 1048 protected function notify_changed($deleted) {
9b5e2461 1049 global $CFG;
82bd6a5e 1050
e01efa2c 1051 // Condition code may cache the grades for conditional availability of
1052 // modules or sections. (This code should use a hook for communication
1053 // with plugin, but hooks are not implemented at time of writing.)
1054 if (!empty($CFG->enableavailability) && class_exists('\availability_grade\callbacks')) {
1055 \availability_grade\callbacks::grade_changed($this->userid);
b85b9b7c
MG
1056 }
1057
4e781c7b 1058 require_once($CFG->libdir.'/completionlib.php');
cb551e36 1059
49f6e5f4 1060 // Bail out immediately if completion is not enabled for site (saves loading
ddf368a2 1061 // grade item & requiring the restore stuff).
cb551e36 1062 if (!completion_info::is_enabled_for_site()) {
49f6e5f4 1063 return;
1064 }
cb551e36 1065
ddf368a2 1066 // Ignore during restore, as completion data will be updated anyway and
1067 // doing it now will result in incorrect dates (it will say they got the
1068 // grade completion now, instead of the correct time).
1069 if (class_exists('restore_controller', false) && restore_controller::is_executing()) {
1070 return;
1071 }
1072
49f6e5f4 1073 // Load information about grade item
1074 $this->load_grade_item();
cb551e36 1075
49f6e5f4 1076 // Only course-modules have completion data
cb551e36 1077 if ($this->grade_item->itemtype!='mod') {
49f6e5f4 1078 return;
1079 }
cb551e36 1080
4e781c7b 1081 // Use $COURSE if available otherwise get it via item fields
9b5e2461 1082 $course = get_course($this->grade_item->courseid, false);
4e781c7b 1083
49f6e5f4 1084 // Bail out if completion is not enabled for course
cb551e36 1085 $completion = new completion_info($course);
1086 if (!$completion->is_enabled()) {
4e781c7b 1087 return;
1088 }
1089
49f6e5f4 1090 // Get course-module
cb551e36 1091 $cm = get_coursemodule_from_instance($this->grade_item->itemmodule,
1092 $this->grade_item->iteminstance, $this->grade_item->courseid);
ad275cb4 1093 // If the course-module doesn't exist, display a warning...
cb551e36 1094 if (!$cm) {
ad275cb4 1095 // ...unless the grade is being deleted in which case it's likely
1096 // that the course-module was just deleted too, so that's okay.
1097 if (!$deleted) {
1098 debugging("Couldn't find course-module for module '" .
1099 $this->grade_item->itemmodule . "', instance '" .
1100 $this->grade_item->iteminstance . "', course '" .
1101 $this->grade_item->courseid . "'");
1102 }
4e781c7b 1103 return;
1104 }
1105
1106 // Pass information on to completion system
cb551e36 1107 $completion->inform_grade_changed($cm, $this->grade_item, $this, $deleted);
bfe969e8
DW
1108 }
1109
1110 /**
1111 * Get some useful information about how this grade_grade is reflected in the aggregation
1112 * for the grade_category. For example this could be an extra credit item, and it could be
1113 * dropped because it's in the X lowest or highest.
1114 *
53771c40 1115 * @return array(status, weight) - A keyword and a numerical weight that represents how this grade was included in the aggregation.
bfe969e8 1116 */
6077a4d4 1117 function get_aggregation_hint() {
53771c40 1118 return array('status' => $this->get_aggregationstatus(),
6077a4d4 1119 'weight' => $this->get_aggregationweight());
bfe969e8 1120 }
e5c674f1 1121}