Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
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/>.
16
 
17
/**
18
 * Definition of a class to represent an individual user's grade
19
 *
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
24
 */
25
 
26
defined('MOODLE_INTERNAL') || die();
27
 
28
require_once('grade_object.php');
29
 
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
 */
38
class grade_grade extends grade_object {
39
 
40
    /**
41
     * The DB table.
42
     * @var string $table
43
     */
44
    public $table = 'grade_grades';
45
 
46
    /**
47
     * Array of required table fields, must start with 'id'.
48
     * @var array $required_fields
49
     */
50
    public $required_fields = array('id', 'itemid', 'userid', 'rawgrade', 'rawgrademax', 'rawgrademin',
51
                                 'rawscaleid', 'usermodified', 'finalgrade', 'hidden', 'locked',
52
                                 'locktime', 'exported', 'overridden', 'excluded', 'timecreated',
53
                                 'timemodified', 'aggregationstatus', 'aggregationweight');
54
 
55
    /**
56
     * Array of optional fields with default values (these should match db defaults)
57
     * @var array $optional_fields
58
     */
59
    public $optional_fields = array('feedback'=>null, 'feedbackformat'=>0, 'information'=>null, 'informationformat'=>0);
60
 
61
    /**
62
     * The id of the grade_item this grade belongs to.
63
     * @var int $itemid
64
     */
65
    public $itemid;
66
 
67
    /**
68
     * The grade_item object referenced by $this->itemid.
69
     * @var grade_item $grade_item
70
     */
71
    public $grade_item;
72
 
73
    /**
74
     * The id of the user this grade belongs to.
75
     * @var int $userid
76
     */
77
    public $userid;
78
 
79
    /**
80
     * The grade value of this raw grade, if such was provided by the module.
81
     * @var float $rawgrade
82
     */
83
    public $rawgrade;
84
 
85
    /**
86
     * The maximum allowable grade when this grade was created.
87
     * @var float $rawgrademax
88
     */
89
    public $rawgrademax = 100;
90
 
91
    /**
92
     * The minimum allowable grade when this grade was created.
93
     * @var float $rawgrademin
94
     */
95
    public $rawgrademin = 0;
96
 
97
    /**
98
     * id of the scale, if this grade is based on a scale.
99
     * @var int $rawscaleid
100
     */
101
    public $rawscaleid;
102
 
103
    /**
104
     * The userid of the person who last modified this grade.
105
     * @var int $usermodified
106
     */
107
    public $usermodified;
108
 
109
    /**
110
     * The final value of this grade.
111
     * @var float $finalgrade
112
     */
113
    public $finalgrade;
114
 
115
    /**
116
     * 0 if visible, 1 always hidden or date not visible until
117
     * @var float $hidden
118
     */
119
    public $hidden = 0;
120
 
121
    /**
122
     * 0 not locked, date when the item was locked
123
     * @var float locked
124
     */
125
    public $locked = 0;
126
 
127
    /**
128
     * 0 no automatic locking, date when to lock the grade automatically
129
     * @var float $locktime
130
     */
131
    public $locktime = 0;
132
 
133
    /**
134
     * Exported flag
135
     * @var bool $exported
136
     */
137
    public $exported = 0;
138
 
139
    /**
140
     * Overridden flag
141
     * @var bool $overridden
142
     */
143
    public $overridden = 0;
144
 
145
    /**
146
     * Grade excluded from aggregation functions
147
     * @var bool $excluded
148
     */
149
    public $excluded = 0;
150
 
151
    /**
152
     * TODO: HACK: create a new field datesubmitted - the date of submission if any (MDL-31377)
153
     * @var bool $timecreated
154
     */
155
    public $timecreated = null;
156
 
157
    /**
158
     * TODO: HACK: create a new field dategraded - the date of grading (MDL-31378)
159
     * @var bool $timemodified
160
     */
161
    public $timemodified = null;
162
 
163
    /**
164
     * Aggregation status flag. Can be one of 'unknown', 'dropped', 'novalue' or 'used'.
165
     * @var string $aggregationstatus
166
     */
167
    public $aggregationstatus = 'unknown';
168
 
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;
174
 
175
    /**
176
     * Feedback files to copy.
177
     *
178
     * Example -
179
     *
180
     * [
181
     *     'contextid' => 1,
182
     *     'component' => 'mod_xyz',
183
     *     'filearea' => 'mod_xyz_feedback',
184
     *     'itemid' => 2
185
     * ];
186
     *
187
     * @var array
188
     */
189
    public $feedbackfiles = [];
190
 
191
    /**
192
     * Feedback content.
193
     * @var string $feedback
194
     */
195
    public $feedback;
196
 
197
    /**
198
     * Feedback format.
199
     * @var int $feedbackformat
200
     */
201
    public $feedbackformat = FORMAT_PLAIN;
202
 
203
    /**
204
     * Information text.
205
     * @var string $information
206
     */
207
    public $information;
208
 
209
    /**
210
     * Information text format.
211
     * @var int $informationformat
212
     */
213
    public $informationformat = FORMAT_PLAIN;
214
 
215
    /**
216
     * label text.
217
     * @var string $label
218
     */
219
    public $label;
220
 
221
    /**
222
     * Returns array of grades for given grade_item+users
223
     *
224
     * @param grade_item $grade_item
225
     * @param array $userids
226
     * @param bool $include_missing include grades that do not exist yet
227
     * @return array userid=>grade_grade array
228
     */
229
    public static function fetch_users_grades($grade_item, $userids, $include_missing=true) {
230
        global $DB;
231
 
232
        // hmm, there might be a problem with length of sql query
233
        // if there are too many users requested - we might run out of memory anyway
234
        $limit = 2000;
235
        $count = count($userids);
236
        if ($count > $limit) {
237
            $half = (int)($count/2);
238
            $first  = array_slice($userids, 0, $half);
239
            $second = array_slice($userids, $half);
240
            return grade_grade::fetch_users_grades($grade_item, $first, $include_missing) + grade_grade::fetch_users_grades($grade_item, $second, $include_missing);
241
        }
242
 
243
        list($user_ids_cvs, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'uid0');
244
        $params['giid'] = $grade_item->id;
245
        $result = array();
246
        if ($grade_records = $DB->get_records_select('grade_grades', "itemid=:giid AND userid $user_ids_cvs", $params)) {
247
            foreach ($grade_records as $record) {
248
                $result[$record->userid] = new grade_grade($record, false);
249
            }
250
        }
251
        if ($include_missing) {
252
            foreach ($userids as $userid) {
253
                if (!array_key_exists($userid, $result)) {
254
                    $grade_grade = new grade_grade();
255
                    $grade_grade->userid = $userid;
256
                    $grade_grade->itemid = $grade_item->id;
257
                    $result[$userid] = $grade_grade;
258
                }
259
            }
260
        }
261
 
262
        return $result;
263
    }
264
 
265
    /**
266
     * Loads the grade_item object referenced by $this->itemid and saves it as $this->grade_item for easy access
267
     *
268
     * @return ?grade_item The grade_item instance referenced by $this->itemid
269
     */
270
    public function load_grade_item() {
271
        if (empty($this->itemid)) {
272
            debugging('Missing itemid');
273
            $this->grade_item = null;
274
            return null;
275
        }
276
 
277
        if (empty($this->grade_item)) {
278
            $this->grade_item = grade_item::fetch(array('id'=>$this->itemid));
279
 
280
        } else if ($this->grade_item->id != $this->itemid) {
281
            debugging('Itemid mismatch');
282
            $this->grade_item = grade_item::fetch(array('id'=>$this->itemid));
283
        }
284
 
285
        if (empty($this->grade_item)) {
286
            debugging("Missing grade item id $this->itemid", DEBUG_DEVELOPER);
287
        }
288
 
289
        return $this->grade_item;
290
    }
291
 
292
    /**
293
     * Is grading object editable?
294
     *
295
     * @return bool
296
     */
297
    public function is_editable() {
298
        if ($this->is_locked()) {
299
            return false;
300
        }
301
 
302
        $grade_item = $this->load_grade_item();
303
 
304
        if ($grade_item->gradetype == GRADE_TYPE_NONE) {
305
            return false;
306
        }
307
 
308
        if ($grade_item->is_course_item() or $grade_item->is_category_item()) {
309
            return (bool)get_config('moodle', 'grade_overridecat');
310
        }
311
 
312
        return true;
313
    }
314
 
315
    /**
316
     * Check grade lock status. Uses both grade item lock and grade lock.
317
     * Internally any date in locked field (including future ones) means locked,
318
     * the date is stored for logging purposes only.
319
     *
320
     * @return bool True if locked, false if not
321
     */
322
    public function is_locked() {
323
        $this->load_grade_item();
324
        if (empty($this->grade_item)) {
325
            return !empty($this->locked);
326
        } else {
327
            return !empty($this->locked) or $this->grade_item->is_locked();
328
        }
329
    }
330
 
331
    /**
332
     * Checks if grade overridden
333
     *
334
     * @return bool True if grade is overriden
335
     */
336
    public function is_overridden() {
337
        return !empty($this->overridden);
338
    }
339
 
340
    /**
341
     * Returns timestamp of submission related to this grade, null if not submitted.
342
     *
343
     * @return int Timestamp
344
     */
345
    public function get_datesubmitted() {
346
        //TODO: HACK - create new fields (MDL-31379)
347
        return $this->timecreated;
348
    }
349
 
350
    /**
351
     * Returns the weight this grade contributed to the aggregated grade
352
     *
353
     * @return float|null
354
     */
355
    public function get_aggregationweight() {
356
        return $this->aggregationweight;
357
    }
358
 
359
    /**
360
     * Set aggregationweight.
361
     *
362
     * @param float $aggregationweight
363
     * @return void
364
     */
365
    public function set_aggregationweight($aggregationweight) {
366
        $this->aggregationweight = $aggregationweight;
367
        $this->update();
368
    }
369
 
370
    /**
371
     * Returns the info on how this value was used in the aggregated grade
372
     *
373
     * @return string One of 'dropped', 'excluded', 'novalue', 'used' or 'extra'
374
     */
375
    public function get_aggregationstatus() {
376
        return $this->aggregationstatus;
377
    }
378
 
379
    /**
380
     * Set aggregationstatus flag
381
     *
382
     * @param string $aggregationstatus
383
     * @return void
384
     */
385
    public function set_aggregationstatus($aggregationstatus) {
386
        $this->aggregationstatus = $aggregationstatus;
387
        $this->update();
388
    }
389
 
390
    /**
391
     * Returns the minimum and maximum number of points this grade is graded with respect to.
392
     *
393
     * @since  Moodle 2.8.7, 2.9.1
394
     * @return array A list containing, in order, the minimum and maximum number of points.
395
     */
396
    protected function get_grade_min_and_max() {
397
        global $CFG;
398
        $this->load_grade_item();
399
 
400
        // When the following setting is turned on we use the grade_grade raw min and max values.
401
        $minmaxtouse = grade_get_setting($this->grade_item->courseid, 'minmaxtouse', $CFG->grade_minmaxtouse);
402
 
403
        // Check to see if the gradebook is frozen. This allows grades to not be altered at all until a user verifies that they
404
        // wish to update the grades.
405
        $gradebookcalculationsfreeze = 'gradebook_calculations_freeze_' . $this->grade_item->courseid;
406
        // Gradebook is frozen, run through old code.
407
        if (isset($CFG->$gradebookcalculationsfreeze) && (int)$CFG->$gradebookcalculationsfreeze <= 20150627) {
408
            // Only aggregate items use separate min grades.
409
            if ($minmaxtouse == GRADE_MIN_MAX_FROM_GRADE_GRADE || $this->grade_item->is_aggregate_item()) {
410
                return array($this->rawgrademin, $this->rawgrademax);
411
            } else {
412
                return array($this->grade_item->grademin, $this->grade_item->grademax);
413
            }
414
        } else {
415
            // Only aggregate items use separate min grades, unless they are calculated grade items.
416
            if (($this->grade_item->is_aggregate_item() && !$this->grade_item->is_calculated())
417
                    || $minmaxtouse == GRADE_MIN_MAX_FROM_GRADE_GRADE) {
418
                return array($this->rawgrademin, $this->rawgrademax);
419
            } else {
420
                return array($this->grade_item->grademin, $this->grade_item->grademax);
421
            }
422
        }
423
    }
424
 
425
    /**
426
     * Returns the minimum number of points this grade is graded with.
427
     *
428
     * @since  Moodle 2.8.7, 2.9.1
429
     * @return float The minimum number of points
430
     */
431
    public function get_grade_min() {
432
        list($min, $max) = $this->get_grade_min_and_max();
433
 
434
        return $min;
435
    }
436
 
437
    /**
438
     * Returns the maximum number of points this grade is graded with respect to.
439
     *
440
     * @since  Moodle 2.8.7, 2.9.1
441
     * @return float The maximum number of points
442
     */
443
    public function get_grade_max() {
444
        list($min, $max) = $this->get_grade_min_and_max();
445
 
446
        return $max;
447
    }
448
 
449
    /**
450
     * Returns timestamp when last graded, null if no grade present
451
     *
452
     * @return ?int
453
     */
454
    public function get_dategraded() {
455
        //TODO: HACK - create new fields (MDL-31379)
456
        if (is_null($this->finalgrade) and is_null($this->feedback)) {
457
            return null; // no grade == no date
458
        } else if ($this->overridden) {
459
            return $this->overridden;
460
        } else {
461
            return $this->timemodified;
462
        }
463
    }
464
 
465
    /**
466
     * Set the overridden status of grade
467
     *
468
     * @param bool $state requested overridden state
469
     * @param bool $refresh refresh grades from external activities if needed
470
     * @return bool true is db state changed
471
     */
472
    public function set_overridden($state, $refresh = true) {
473
        if (empty($this->overridden) and $state) {
474
            $this->overridden = time();
475
            $this->update(null, true);
476
            return true;
477
 
478
        } else if (!empty($this->overridden) and !$state) {
479
            $this->overridden = 0;
480
            $this->update(null, true);
481
 
482
            if ($refresh) {
483
                //refresh when unlocking
484
                $this->grade_item->refresh_grades($this->userid);
485
            }
486
 
487
            return true;
488
        }
489
        return false;
490
    }
491
 
492
    /**
493
     * Checks if grade excluded from aggregation functions
494
     *
495
     * @return bool True if grade is excluded from aggregation
496
     */
497
    public function is_excluded() {
498
        return !empty($this->excluded);
499
    }
500
 
501
    /**
502
     * Set the excluded status of grade
503
     *
504
     * @param bool $state requested excluded state
505
     * @return bool True is database state changed
506
     */
507
    public function set_excluded($state) {
508
        if (empty($this->excluded) and $state) {
509
            $this->excluded = time();
510
            $this->update();
511
            return true;
512
 
513
        } else if (!empty($this->excluded) and !$state) {
514
            $this->excluded = 0;
515
            $this->update();
516
            return true;
517
        }
518
        return false;
519
    }
520
 
521
    /**
522
     * Lock/unlock this grade.
523
     *
524
     * @param int $lockedstate 0, 1 or a timestamp int(10) after which date the item will be locked.
525
     * @param bool $cascade Ignored param
526
     * @param bool $refresh Refresh grades when unlocking
527
     * @return bool True if successful, false if can not set new lock state for grade
528
     */
529
    public function set_locked($lockedstate, $cascade=false, $refresh=true) {
530
        $this->load_grade_item();
531
 
532
        if ($lockedstate) {
533
            if ($this->grade_item->needsupdate) {
534
                //can not lock grade if final not calculated!
535
                return false;
536
            }
537
 
538
            $this->locked = time();
539
            $this->update();
540
 
541
            return true;
542
 
543
        } else {
544
            if (!empty($this->locked) and $this->locktime < time()) {
545
                //we have to reset locktime or else it would lock up again
546
                $this->locktime = 0;
547
            }
548
 
549
            // remove the locked flag
550
            $this->locked = 0;
551
            $this->update();
552
 
553
            if ($refresh and !$this->is_overridden()) {
554
                //refresh when unlocking and not overridden
555
                $this->grade_item->refresh_grades($this->userid);
556
            }
557
 
558
            return true;
559
        }
560
    }
561
 
562
    /**
563
     * Lock the grade if needed. Make sure this is called only when final grades are valid
564
     *
565
     * @param array $items array of all grade item ids
566
     * @return void
567
     */
568
    public static function check_locktime_all($items) {
569
        global $CFG, $DB;
570
 
571
        $now = time(); // no rounding needed, this is not supposed to be called every 10 seconds
572
        list($usql, $params) = $DB->get_in_or_equal($items);
573
        $params[] = $now;
574
        $rs = $DB->get_recordset_select('grade_grades', "itemid $usql AND locked = 0 AND locktime > 0 AND locktime < ?", $params);
575
        foreach ($rs as $grade) {
576
            $grade_grade = new grade_grade($grade, false);
577
            $grade_grade->locked = time();
578
            $grade_grade->update('locktime');
579
        }
580
        $rs->close();
581
    }
582
 
583
    /**
584
     * Set the locktime for this grade.
585
     *
586
     * @param int $locktime timestamp for lock to activate
587
     * @return void
588
     */
589
    public function set_locktime($locktime) {
590
        $this->locktime = $locktime;
591
        $this->update();
592
    }
593
 
594
    /**
595
     * Get the locktime for this grade.
596
     *
597
     * @return int $locktime timestamp for lock to activate
598
     */
599
    public function get_locktime() {
600
        $this->load_grade_item();
601
 
602
        $item_locktime = $this->grade_item->get_locktime();
603
 
604
        if (empty($this->locktime) or ($item_locktime and $item_locktime < $this->locktime)) {
605
            return $item_locktime;
606
 
607
        } else {
608
            return $this->locktime;
609
        }
610
    }
611
 
612
    /**
613
     * Check grade hidden status. Uses data from both grade item and grade.
614
     *
615
     * @return bool true if hidden, false if not
616
     */
617
    public function is_hidden() {
618
        $this->load_grade_item();
619
        if (empty($this->grade_item)) {
620
            return $this->hidden == 1 or ($this->hidden != 0 and $this->hidden > time());
621
        } else {
622
            return $this->hidden == 1 or ($this->hidden != 0 and $this->hidden > time()) or $this->grade_item->is_hidden();
623
        }
624
    }
625
 
626
    /**
627
     * Check grade hidden status. Uses data from both grade item and grade.
628
     *
629
     * @return bool true if hiddenuntil, false if not
630
     */
631
    public function is_hiddenuntil() {
632
        $this->load_grade_item();
633
 
634
        if ($this->hidden == 1 or $this->grade_item->hidden == 1) {
635
            return false; //always hidden
636
        }
637
 
638
        if ($this->hidden > 1 or $this->grade_item->hidden > 1) {
639
            return true;
640
        }
641
 
642
        return false;
643
    }
644
 
645
    /**
646
     * Check grade hidden status. Uses data from both grade item and grade.
647
     *
648
     * @return int 0 means visible, 1 hidden always, timestamp hidden until
649
     */
650
    public function get_hidden() {
651
        $this->load_grade_item();
652
 
653
        $item_hidden = $this->grade_item->get_hidden();
654
 
655
        if ($item_hidden == 1) {
656
            return 1;
657
 
658
        } else if ($item_hidden == 0) {
659
            return $this->hidden;
660
 
661
        } else {
662
            if ($this->hidden == 0) {
663
                return $item_hidden;
664
            } else if ($this->hidden == 1) {
665
                return 1;
666
            } else if ($this->hidden > $item_hidden) {
667
                return $this->hidden;
668
            } else {
669
                return $item_hidden;
670
            }
671
        }
672
    }
673
 
674
    /**
675
     * Set the hidden status of grade, 0 mean visible, 1 always hidden, number means date to hide until.
676
     *
677
     * @param int $hidden new hidden status
678
     * @param bool $cascade ignored
679
     */
680
    public function set_hidden($hidden, $cascade=false) {
681
       $this->hidden = $hidden;
682
       $this->update();
683
    }
684
 
685
    /**
686
     * Finds and returns a grade_grade instance based on params.
687
     *
688
     * @param array $params associative arrays varname=>value
689
     * @return grade_grade Returns a grade_grade instance or false if none found
690
     */
691
    public static function fetch($params) {
692
        return grade_object::fetch_helper('grade_grades', 'grade_grade', $params);
693
    }
694
 
695
    /**
696
     * Finds and returns all grade_grade instances based on params.
697
     *
698
     * @param array $params associative arrays varname=>value
699
     * @return array array of grade_grade instances or false if none found.
700
     */
701
    public static function fetch_all($params) {
702
        return grade_object::fetch_all_helper('grade_grades', 'grade_grade', $params);
703
    }
704
 
705
    /**
706
     * Given a float value situated between a source minimum and a source maximum, converts it to the
707
     * corresponding value situated between a target minimum and a target maximum. Thanks to Darlene
708
     * for the formula :-)
709
     *
710
     * @param float $rawgrade
711
     * @param float $source_min
712
     * @param float $source_max
713
     * @param float $target_min
714
     * @param float $target_max
715
     * @return ?float Converted value
716
     */
717
    public static function standardise_score($rawgrade, $source_min, $source_max, $target_min, $target_max) {
718
        if (is_null($rawgrade)) {
719
          return null;
720
        }
721
 
722
        if ($source_max == $source_min or $target_min == $target_max) {
723
            // prevent division by 0
724
            return $target_max;
725
        }
726
 
727
        $factor = ($rawgrade - $source_min) / ($source_max - $source_min);
728
        $diff = $target_max - $target_min;
729
        $standardised_value = $factor * $diff + $target_min;
730
        return $standardised_value;
731
    }
732
 
733
    /**
734
     * Given an array like this:
735
     * $a = array(1=>array(2, 3),
736
     *            2=>array(4),
737
     *            3=>array(1),
738
     *            4=>array())
739
     * this function fully resolves the dependencies so each value will be an array of
740
     * the all items this item depends on and their dependencies (and their dependencies...).
741
     * It should not explode if there are circular dependencies.
742
     * The dependency depth array will list the number of branches in the tree above each leaf.
743
     *
744
     * @param array $dependson Array to flatten
745
     * @param array $dependencydepth Array of itemids => depth. Initially these should be all set to 1.
746
     * @return bool|null
747
     */
748
    protected static function flatten_dependencies_array(&$dependson, &$dependencydepth) {
749
        // Flatten the nested dependencies - this will handle recursion bombs because it removes duplicates.
750
        $somethingchanged = true;
751
        // First of all, delete any incorrect (not array or individual null) dependency, they aren't welcome.
752
        // TODO: Maybe we should report about this happening, it shouldn't if all dependencies are correct and consistent.
753
        foreach ($dependson as $itemid => $depends) {
754
            $depends = is_array($depends) ? $depends : []; // Only arrays are accepted.
755
            $dependson[$itemid] = array_filter($depends, function($val) { // Only not-null values are accepted.
756
                return !is_null($val);
757
            });
758
        }
759
        while ($somethingchanged) {
760
            $somethingchanged = false;
761
 
762
            foreach ($dependson as $itemid => $depends) {
763
                // Make a copy so we can tell if it changed.
764
                $before = $dependson[$itemid];
765
                foreach ($depends as $subitemid => $subdepends) {
766
                    $dependson[$itemid] = array_unique(array_merge($depends, $dependson[$subdepends] ?? []));
767
                    sort($dependson[$itemid], SORT_NUMERIC);
768
                }
769
                if ($before != $dependson[$itemid]) {
770
                    $somethingchanged = true;
771
                    if (!isset($dependencydepth[$itemid])) {
772
                        $dependencydepth[$itemid] = 1;
773
                    } else {
774
                        $dependencydepth[$itemid]++;
775
                    }
776
                }
777
            }
778
        }
779
    }
780
 
781
    /**
782
     * Return array of grade item ids that are either hidden or indirectly depend
783
     * on hidden grades, excluded grades are not returned.
784
     * THIS IS A REALLY BIG HACK! to be replaced by conditional aggregation of hidden grades in 2.0
785
     *
786
     * @param array $grade_grades all course grades of one user, & used for better internal caching
787
     * @param array $grade_items array of grade items, & used for better internal caching
788
     * @return array This is an array of following arrays:
789
     *      unknown => list of item ids that may be affected by hiding (with the ITEM ID as both the key and the value) - for BC with old gradereport plugins
790
     *      unknowngrades => list of item ids that may be affected by hiding (with the calculated grade as the value)
791
     *      altered => list of item ids that are definitely affected by hiding (with the calculated grade as the value)
792
     *      alteredgrademax => for each item in altered or unknown, the new value of the grademax
793
     *      alteredgrademin => for each item in altered or unknown, the new value of the grademin
794
     *      alteredgradestatus => for each item with a modified status - the value of the new status
795
     *      alteredgradeweight => for each item with a modified weight - the value of the new weight
796
     */
797
    public static function get_hiding_affected(&$grade_grades, &$grade_items) {
798
        global $CFG;
799
 
800
        if (count($grade_grades) !== count($grade_items)) {
801
            throw new \moodle_exception('invalidarraysize', 'debug', '', 'grade_grade::get_hiding_affected()!');
802
        }
803
 
804
        $dependson = array();
805
        $todo = array();
806
        $unknown = array();  // can not find altered
807
        $altered = array();  // altered grades
808
        $alteredgrademax = array();  // Altered grade max values.
809
        $alteredgrademin = array();  // Altered grade min values.
810
        $alteredaggregationstatus = array();  // Altered aggregation status.
811
        $alteredaggregationweight = array();  // Altered aggregation weight.
812
        $dependencydepth = array();
813
 
814
        $hiddenfound = false;
815
        foreach($grade_grades as $itemid=>$unused) {
816
            $grade_grade =& $grade_grades[$itemid];
817
            // We need the immediate dependencies of all every grade_item so we can calculate nested dependencies.
818
            $dependson[$grade_grade->itemid] = $grade_items[$grade_grade->itemid]->depends_on();
819
            if ($grade_grade->is_excluded()) {
820
                //nothing to do, aggregation is ok
821
                continue;
822
            } else if ($grade_grade->is_hidden()) {
823
                $hiddenfound = true;
824
                $altered[$grade_grade->itemid] = null;
825
                $alteredaggregationstatus[$grade_grade->itemid] = 'dropped';
826
                $alteredaggregationweight[$grade_grade->itemid] = 0;
827
            } else if ($grade_grade->is_overridden()) {
828
                // No need to recalculate overridden grades.
829
                continue;
830
            } else {
831
                if (!empty($dependson[$grade_grade->itemid])) {
832
                    $dependencydepth[$grade_grade->itemid] = 1;
833
                    $todo[] = $grade_grade->itemid;
834
                }
835
            }
836
        }
837
 
838
        // Flatten the dependency tree and count number of branches to each leaf.
839
        self::flatten_dependencies_array($dependson, $dependencydepth);
840
 
841
        if (!$hiddenfound) {
842
            return array('unknown' => array(),
843
                         'unknowngrades' => array(),
844
                         'altered' => array(),
845
                         'alteredgrademax' => array(),
846
                         'alteredgrademin' => array(),
847
                         'alteredaggregationstatus' => array(),
848
                         'alteredaggregationweight' => array());
849
        }
850
        // This line ensures that $dependencydepth has the same number of items as $todo.
851
        $dependencydepth = array_intersect_key($dependencydepth, array_flip($todo));
852
        // We need to resort the todo list by the dependency depth. This guarantees we process the leaves, then the branches.
853
        array_multisort($dependencydepth, $todo);
854
 
855
        $max = count($todo);
856
        $hidden_precursors = null;
857
        for($i=0; $i<$max; $i++) {
858
            $found = false;
859
            foreach($todo as $key=>$do) {
860
                $hidden_precursors = array_intersect($dependson[$do], array_keys($unknown));
861
                if ($hidden_precursors) {
862
                    // this item depends on hidden grade indirectly
863
                    $unknown[$do] = $grade_grades[$do]->finalgrade;
864
                    unset($todo[$key]);
865
                    $found = true;
866
                    continue;
867
 
868
                } else if (!array_intersect($dependson[$do], $todo)) {
869
                    $hidden_precursors = array_intersect($dependson[$do], array_keys($altered));
870
                    // If the dependency is a sum aggregation, we need to process it as if it had hidden items.
871
                    // The reason for this, is that the code will recalculate the maxgrade by removing ungraded
872
                    // items and accounting for 'drop x grades' and then stored back in our virtual grade_items.
873
                    // This recalculation is necessary because there will be a call to:
874
                    //              $grade_category->aggregate_values_and_adjust_bounds
875
                    // for the top level grade that will depend on knowing what that caclulated grademax is
876
                    // and it finds that value by checking the virtual grade_items.
877
                    $issumaggregate = false;
878
                    if ($grade_items[$do]->itemtype == 'category') {
879
                        $issumaggregate = $grade_items[$do]->load_item_category()->aggregation == GRADE_AGGREGATE_SUM;
880
                    }
881
                    if (!$hidden_precursors && !$issumaggregate) {
882
                        unset($todo[$key]);
883
                        $found = true;
884
                        continue;
885
 
886
                    } else {
887
                        // depends on altered grades - we should try to recalculate if possible
888
                        if ($grade_items[$do]->is_calculated() or
889
                            (!$grade_items[$do]->is_category_item() and !$grade_items[$do]->is_course_item()) or
890
                            ($grade_items[$do]->is_category_item() and $grade_items[$do]->is_locked())
891
                        ) {
892
                            // This is a grade item that is not a category or course and has been affected by grade hiding.
893
                            // Or a grade item that is a category and it is locked.
894
                            // I guess this means it is a calculation that needs to be recalculated.
895
                            $unknown[$do] = $grade_grades[$do]->finalgrade;
896
                            unset($todo[$key]);
897
                            $found = true;
898
                            continue;
899
 
900
                        } else {
901
                            // This is a grade category (or course).
902
                            $grade_category = $grade_items[$do]->load_item_category();
903
 
904
                            // Build a new list of the grades in this category.
905
                            $values = array();
906
                            $immediatedepends = $grade_items[$do]->depends_on();
907
                            foreach ($immediatedepends as $itemid) {
908
                                if (array_key_exists($itemid, $altered)) {
909
                                    //nulling an altered precursor
910
                                    $values[$itemid] = $altered[$itemid];
911
                                    if (is_null($values[$itemid])) {
912
                                        // This means this was a hidden grade item removed from the result.
913
                                        unset($values[$itemid]);
914
                                    }
915
                                } elseif (empty($values[$itemid])) {
916
                                    $values[$itemid] = $grade_grades[$itemid]->finalgrade;
917
                                }
918
                            }
919
 
920
                            foreach ($values as $itemid=>$value) {
921
                                if ($grade_grades[$itemid]->is_excluded()) {
922
                                    unset($values[$itemid]);
923
                                    $alteredaggregationstatus[$itemid] = 'excluded';
924
                                    $alteredaggregationweight[$itemid] = null;
925
                                    continue;
926
                                }
927
                                // The grade min/max may have been altered by hiding.
928
                                $grademin = $grade_items[$itemid]->grademin;
929
                                if (isset($alteredgrademin[$itemid])) {
930
                                    $grademin = $alteredgrademin[$itemid];
931
                                }
932
                                $grademax = $grade_items[$itemid]->grademax;
933
                                if (isset($alteredgrademax[$itemid])) {
934
                                    $grademax = $alteredgrademax[$itemid];
935
                                }
936
                                $values[$itemid] = grade_grade::standardise_score($value, $grademin, $grademax, 0, 1);
937
                            }
938
 
939
                            if ($grade_category->aggregateonlygraded) {
940
                                foreach ($values as $itemid=>$value) {
941
                                    if (is_null($value)) {
942
                                        unset($values[$itemid]);
943
                                        $alteredaggregationstatus[$itemid] = 'novalue';
944
                                        $alteredaggregationweight[$itemid] = null;
945
                                    }
946
                                }
947
                            } else {
948
                                foreach ($values as $itemid=>$value) {
949
                                    if (is_null($value)) {
950
                                        $values[$itemid] = 0;
951
                                    }
952
                                }
953
                            }
954
 
955
                            // limit and sort
956
                            $allvalues = $values;
957
                            $grade_category->apply_limit_rules($values, $grade_items);
958
 
959
                            $moredropped = array_diff($allvalues, $values);
960
                            foreach ($moredropped as $drop => $unused) {
961
                                $alteredaggregationstatus[$drop] = 'dropped';
962
                                $alteredaggregationweight[$drop] = null;
963
                            }
964
 
965
                            foreach ($values as $itemid => $val) {
966
                                if ($grade_category->is_extracredit_used() && ($grade_items[$itemid]->aggregationcoef > 0)) {
967
                                    $alteredaggregationstatus[$itemid] = 'extra';
968
                                }
969
                            }
970
 
971
                            asort($values, SORT_NUMERIC);
972
 
973
                            // let's see we have still enough grades to do any statistics
974
                            if (count($values) == 0) {
975
                                // not enough attempts yet
976
                                $altered[$do] = null;
977
                                unset($todo[$key]);
978
                                $found = true;
979
                                continue;
980
                            }
981
 
982
                            $usedweights = array();
983
                            $adjustedgrade = $grade_category->aggregate_values_and_adjust_bounds($values, $grade_items, $usedweights);
984
 
985
                            // recalculate the rawgrade back to requested range
986
                            $finalgrade = grade_grade::standardise_score($adjustedgrade['grade'],
987
                                                                         0,
988
                                                                         1,
989
                                                                         $adjustedgrade['grademin'],
990
                                                                         $adjustedgrade['grademax']);
991
 
992
                            foreach ($usedweights as $itemid => $weight) {
993
                                if (!isset($alteredaggregationstatus[$itemid])) {
994
                                    $alteredaggregationstatus[$itemid] = 'used';
995
                                }
996
                                $alteredaggregationweight[$itemid] = $weight;
997
                            }
998
 
999
                            $finalgrade = $grade_items[$do]->bounded_grade($finalgrade);
1000
                            $alteredgrademin[$do] = $adjustedgrade['grademin'];
1001
                            $alteredgrademax[$do] = $adjustedgrade['grademax'];
1002
                            // We need to muck with the "in-memory" grade_items records so
1003
                            // that subsequent calculations will use the adjusted grademin and grademax.
1004
                            $grade_items[$do]->grademin = $adjustedgrade['grademin'];
1005
                            $grade_items[$do]->grademax = $adjustedgrade['grademax'];
1006
 
1007
                            $altered[$do] = $finalgrade;
1008
                            unset($todo[$key]);
1009
                            $found = true;
1010
                            continue;
1011
                        }
1012
                    }
1013
                }
1014
            }
1015
            if (!$found) {
1016
                break;
1017
            }
1018
        }
1019
 
1020
        return array('unknown' => array_combine(array_keys($unknown), array_keys($unknown)), // Left for BC in case some gradereport plugins expect it.
1021
                     'unknowngrades' => $unknown,
1022
                     'altered' => $altered,
1023
                     'alteredgrademax' => $alteredgrademax,
1024
                     'alteredgrademin' => $alteredgrademin,
1025
                     'alteredaggregationstatus' => $alteredaggregationstatus,
1026
                     'alteredaggregationweight' => $alteredaggregationweight);
1027
    }
1028
 
1029
    /**
1030
     * Returns true if the grade's value is superior or equal to the grade item's gradepass value, false otherwise.
1031
     *
1032
     * @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
1033
     * @return ?bool
1034
     */
1035
    public function is_passed($grade_item = null) {
1036
        if (empty($grade_item)) {
1037
            if (!isset($this->grade_item)) {
1038
                $this->load_grade_item();
1039
            }
1040
        } else {
1041
            $this->grade_item = $grade_item;
1042
            $this->itemid = $grade_item->id;
1043
        }
1044
 
1045
        // Return null if finalgrade is null
1046
        if (is_null($this->finalgrade)) {
1047
            return null;
1048
        }
1049
 
1050
        // Return null if gradepass == grademin, gradepass is null, or grade item is a scale and gradepass is 0.
1051
        if (is_null($this->grade_item->gradepass)) {
1052
            return null;
1053
        } else if ($this->grade_item->gradepass == $this->grade_item->grademin) {
1054
            return null;
1055
        } else if ($this->grade_item->gradetype == GRADE_TYPE_SCALE && !grade_floats_different($this->grade_item->gradepass, 0.0)) {
1056
            return null;
1057
        }
1058
 
1059
        return $this->finalgrade >= $this->grade_item->gradepass;
1060
    }
1061
 
1062
    /**
1063
     * In addition to update() as defined in grade_object rounds the float numbers using php function,
1064
     * the reason is we need to compare the db value with computed number to skip updates if possible.
1065
     *
1066
     * @param string $source from where was the object inserted (mod/forum, manual, etc.)
1067
     * @param bool $isbulkupdate If bulk grade update is happening.
1068
     * @return bool success
1069
     */
1070
    public function update($source=null, $isbulkupdate = false) {
1071
        $this->rawgrade = grade_floatval($this->rawgrade);
1072
        $this->finalgrade = grade_floatval($this->finalgrade);
1073
        $this->rawgrademin = grade_floatval($this->rawgrademin);
1074
        $this->rawgrademax = grade_floatval($this->rawgrademax);
1075
        return parent::update($source, $isbulkupdate);
1076
    }
1077
 
1078
 
1079
    /**
1080
     * Handles adding feedback files in the gradebook.
1081
     *
1082
     * @param int|null $historyid
1083
     */
1084
    protected function add_feedback_files(int $historyid = null) {
1085
        global $CFG;
1086
 
1087
        // We only support feedback files for modules atm.
1088
        if ($this->grade_item && $this->grade_item->is_external_item()) {
1089
            $context = $this->get_context();
1090
            $this->copy_feedback_files($context, GRADE_FEEDBACK_FILEAREA, $this->id);
1091
 
1092
            if (empty($CFG->disablegradehistory) && $historyid) {
1093
                $this->copy_feedback_files($context, GRADE_HISTORY_FEEDBACK_FILEAREA, $historyid);
1094
            }
1095
        }
1096
 
1097
        return $this->id;
1098
    }
1099
 
1100
    /**
1101
     * Handles updating feedback files in the gradebook.
1102
     *
1103
     * @param int|null $historyid
1104
     */
1105
    protected function update_feedback_files(int $historyid = null) {
1106
        global $CFG;
1107
 
1108
        // We only support feedback files for modules atm.
1109
        if ($this->grade_item && $this->grade_item->is_external_item()) {
1110
            $context = $this->get_context();
1111
 
1112
            $fs = new file_storage();
1113
            $fs->delete_area_files($context->id, GRADE_FILE_COMPONENT, GRADE_FEEDBACK_FILEAREA, $this->id);
1114
 
1115
            $this->copy_feedback_files($context, GRADE_FEEDBACK_FILEAREA, $this->id);
1116
 
1117
            if (empty($CFG->disablegradehistory) && $historyid) {
1118
                $this->copy_feedback_files($context, GRADE_HISTORY_FEEDBACK_FILEAREA, $historyid);
1119
            }
1120
        }
1121
 
1122
        return true;
1123
    }
1124
 
1125
    /**
1126
     * Handles deleting feedback files in the gradebook.
1127
     */
1128
    protected function delete_feedback_files() {
1129
        // We only support feedback files for modules atm.
1130
        if ($this->grade_item && $this->grade_item->is_external_item()) {
1131
            $context = $this->get_context();
1132
 
1133
            $fs = new file_storage();
1134
            $fs->delete_area_files($context->id, GRADE_FILE_COMPONENT, GRADE_FEEDBACK_FILEAREA, $this->id);
1135
 
1136
            // Grade history only gets deleted when we delete the whole grade item.
1137
        }
1138
 
1139
        return true;
1140
    }
1141
 
1142
    /**
1143
     * Deletes the grade_grade instance from the database.
1144
     *
1145
     * @param string $source The location the deletion occurred (mod/forum, manual, etc.).
1146
     * @return bool Returns true if the deletion was successful, false otherwise.
1147
     */
1148
    public function delete($source = null) {
1149
        global $DB;
1150
        try {
1151
            $transaction = $DB->start_delegated_transaction();
1152
            $success = parent::delete($source);
1153
            // If the grade was deleted successfully trigger a grade_deleted event.
1154
            if ($success && !empty($this->grade_item)) {
1155
                $this->load_grade_item();
1156
                \core\event\grade_deleted::create_from_grade($this)->trigger();
1157
            }
1158
            $transaction->allow_commit();
1159
        } catch (Exception $e) {
1160
            $transaction->rollback($e);
1161
        }
1162
        return $success;
1163
    }
1164
 
1165
    /**
1166
     * Used to notify the completion system (if necessary) that a user's grade
1167
     * has changed, and clear up a possible score cache.
1168
     *
1169
     * @param bool $deleted True if grade was actually deleted
1170
     * @param bool $isbulkupdate If bulk grade update is happening.
1171
     */
1172
    protected function notify_changed($deleted, $isbulkupdate = false) {
1173
        global $CFG;
1174
 
1175
        // Condition code may cache the grades for conditional availability of
1176
        // modules or sections. (This code should use a hook for communication
1177
        // with plugin, but hooks are not implemented at time of writing.)
1178
        if (!empty($CFG->enableavailability) && class_exists('\availability_grade\callbacks')) {
1179
            \availability_grade\callbacks::grade_changed($this->userid);
1180
        }
1181
 
1182
        require_once($CFG->libdir.'/completionlib.php');
1183
 
1184
        // Bail out immediately if completion is not enabled for site (saves loading
1185
        // grade item & requiring the restore stuff).
1186
        if (!completion_info::is_enabled_for_site()) {
1187
            return;
1188
        }
1189
 
1190
        // Ignore during restore, as completion data will be updated anyway and
1191
        // doing it now will result in incorrect dates (it will say they got the
1192
        // grade completion now, instead of the correct time).
1193
        if (class_exists('restore_controller', false) && restore_controller::is_executing()) {
1194
            return;
1195
        }
1196
 
1197
        // Load information about grade item, exit if the grade item is missing.
1198
        if (!$this->load_grade_item()) {
1199
            return;
1200
        }
1201
 
1202
        // Only course-modules have completion data
1203
        if ($this->grade_item->itemtype!='mod') {
1204
            return;
1205
        }
1206
 
1207
        // Use $COURSE if available otherwise get it via item fields
1208
        $course = get_course($this->grade_item->courseid, false);
1209
 
1210
        // Bail out if completion is not enabled for course
1211
        $completion = new completion_info($course);
1212
        if (!$completion->is_enabled()) {
1213
            return;
1214
        }
1215
 
1216
        // Get course-module
1217
        $cm = get_coursemodule_from_instance($this->grade_item->itemmodule,
1218
              $this->grade_item->iteminstance, $this->grade_item->courseid);
1219
        // If the course-module doesn't exist, display a warning...
1220
        if (!$cm) {
1221
            // ...unless the grade is being deleted in which case it's likely
1222
            // that the course-module was just deleted too, so that's okay.
1223
            if (!$deleted) {
1224
                debugging("Couldn't find course-module for module '" .
1225
                        $this->grade_item->itemmodule . "', instance '" .
1226
                        $this->grade_item->iteminstance . "', course '" .
1227
                        $this->grade_item->courseid . "'");
1228
            }
1229
            return;
1230
        }
1231
 
1232
        // Pass information on to completion system
1233
        $completion->inform_grade_changed($cm, $this->grade_item, $this, $deleted, $isbulkupdate);
1234
    }
1235
 
1236
    /**
1237
     * Get some useful information about how this grade_grade is reflected in the aggregation
1238
     * for the grade_category. For example this could be an extra credit item, and it could be
1239
     * dropped because it's in the X lowest or highest.
1240
     *
1241
     * @return array(status, weight) - A keyword and a numerical weight that represents how this grade was included in the aggregation.
1242
     */
1243
    function get_aggregation_hint() {
1244
        return array('status' => $this->get_aggregationstatus(),
1245
                     'weight' => $this->get_aggregationweight());
1246
    }
1247
 
1248
    /**
1249
     * Handles copying feedback files to a specified gradebook file area.
1250
     *
1251
     * @param context $context
1252
     * @param string $filearea
1253
     * @param int $itemid
1254
     */
1255
    private function copy_feedback_files(context $context, string $filearea, int $itemid) {
1256
        if ($this->feedbackfiles) {
1257
            $filestocopycontextid = $this->feedbackfiles['contextid'];
1258
            $filestocopycomponent = $this->feedbackfiles['component'];
1259
            $filestocopyfilearea = $this->feedbackfiles['filearea'];
1260
            $filestocopyitemid = $this->feedbackfiles['itemid'];
1261
 
1262
            $fs = new file_storage();
1263
            if ($filestocopy = $fs->get_area_files($filestocopycontextid, $filestocopycomponent, $filestocopyfilearea,
1264
                    $filestocopyitemid)) {
1265
                foreach ($filestocopy as $filetocopy) {
1266
                    $destination = [
1267
                        'contextid' => $context->id,
1268
                        'component' => GRADE_FILE_COMPONENT,
1269
                        'filearea' => $filearea,
1270
                        'itemid' => $itemid
1271
                    ];
1272
                    $fs->create_file_from_storedfile($destination, $filetocopy);
1273
                }
1274
            }
1275
        }
1276
    }
1277
 
1278
    /**
1279
     * Determine the correct context for this grade_grade.
1280
     *
1281
     * @return context
1282
     */
1283
    public function get_context() {
1284
        $this->load_grade_item();
1285
        return $this->grade_item->get_context();
1286
    }
1287
}