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 a grade item
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
require_once('grade_object.php');
28
 
29
/**
30
 * Class representing a grade item.
31
 *
32
 * It is responsible for handling its DB representation, modifying and returning its metadata.
33
 *
34
 * @package   core_grades
35
 * @category  grade
36
 * @copyright 2006 Nicolas Connault
37
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
38
 */
39
class grade_item extends grade_object {
40
    /**
41
     * DB Table (used by grade_object).
42
     * @var string $table
43
     */
44
    public $table = 'grade_items';
45
 
46
    /**
47
     * Array of required table fields, must start with 'id'.
48
     * @var array $required_fields
49
     */
50
    public $required_fields = array('id', 'courseid', 'categoryid', 'itemname', 'itemtype', 'itemmodule', 'iteminstance',
51
                                 'itemnumber', 'iteminfo', 'idnumber', 'calculation', 'gradetype', 'grademax', 'grademin',
52
                                 'scaleid', 'outcomeid', 'gradepass', 'multfactor', 'plusfactor', 'aggregationcoef',
53
                                 'aggregationcoef2', 'sortorder', 'display', 'decimals', 'hidden', 'locked', 'locktime',
54
                                 'needsupdate', 'weightoverride', 'timecreated', 'timemodified');
55
 
56
    /**
57
     * The course this grade_item belongs to.
58
     * @var int $courseid
59
     */
60
    public $courseid;
61
 
62
    /**
63
     * The category this grade_item belongs to (optional).
64
     * @var int $categoryid
65
     */
66
    public $categoryid;
67
 
68
    /**
69
     * The grade_category object referenced $this->iteminstance if itemtype == 'category' or == 'course'.
70
     * @var grade_category $item_category
71
     */
72
    public $item_category;
73
 
74
    /**
75
     * The grade_category object referenced by $this->categoryid.
76
     * @var grade_category $parent_category
77
     */
78
    public $parent_category;
79
 
80
 
81
    /**
82
     * The name of this grade_item (pushed by the module).
83
     * @var string $itemname
84
     */
85
    public $itemname;
86
 
87
    /**
88
     * e.g. 'category', 'course' and 'mod', 'blocks', 'import', etc...
89
     * @var string $itemtype
90
     */
91
    public $itemtype;
92
 
93
    /**
94
     * The module pushing this grade (e.g. 'forum', 'quiz', 'assignment' etc).
95
     * @var string $itemmodule
96
     */
97
    public $itemmodule;
98
 
99
    /**
100
     * ID of the item module
101
     * @var int $iteminstance
102
     */
103
    public $iteminstance;
104
 
105
    /**
106
     * Number of the item in a series of multiple grades pushed by an activity.
107
     * @var int $itemnumber
108
     */
109
    public $itemnumber;
110
 
111
    /**
112
     * Info and notes about this item.
113
     * @var string $iteminfo
114
     */
115
    public $iteminfo;
116
 
117
    /**
118
     * Arbitrary idnumber provided by the module responsible.
119
     * @var string $idnumber
120
     */
121
    public $idnumber;
122
 
123
    /**
124
     * Calculation string used for this item.
125
     * @var string $calculation
126
     */
127
    public $calculation;
128
 
129
    /**
130
     * Indicates if we already tried to normalize the grade calculation formula.
131
     * This flag helps to minimize db access when broken formulas used in calculation.
132
     * @var bool
133
     */
134
    public $calculation_normalized;
135
    /**
136
     * Math evaluation object
137
     * @var calc_formula A formula object
138
     */
139
    public $formula;
140
 
141
    /**
142
     * The type of grade (0 = none, 1 = value, 2 = scale, 3 = text)
143
     * @var int $gradetype
144
     */
145
    public $gradetype = GRADE_TYPE_VALUE;
146
 
147
    /**
148
     * Maximum allowable grade.
149
     * @var float $grademax
150
     */
151
    public $grademax = 100;
152
 
153
    /**
154
     * Minimum allowable grade.
155
     * @var float $grademin
156
     */
157
    public $grademin = 0;
158
 
159
    /**
160
     * id of the scale, if this grade is based on a scale.
161
     * @var int $scaleid
162
     */
163
    public $scaleid;
164
 
165
    /**
166
     * The grade_scale object referenced by $this->scaleid.
167
     * @var grade_scale $scale
168
     */
169
    public $scale;
170
 
171
    /**
172
     * The id of the optional grade_outcome associated with this grade_item.
173
     * @var int $outcomeid
174
     */
175
    public $outcomeid;
176
 
177
    /**
178
     * The grade_outcome this grade is associated with, if applicable.
179
     * @var grade_outcome $outcome
180
     */
181
    public $outcome;
182
 
183
    /**
184
     * grade required to pass. (grademin <= gradepass <= grademax)
185
     * @var float $gradepass
186
     */
187
    public $gradepass = 0;
188
 
189
    /**
190
     * Multiply all grades by this number.
191
     * @var float $multfactor
192
     */
193
    public $multfactor = 1.0;
194
 
195
    /**
196
     * Add this to all grades.
197
     * @var float $plusfactor
198
     */
199
    public $plusfactor = 0;
200
 
201
    /**
202
     * Aggregation coeficient used for weighted averages or extra credit
203
     * @var float $aggregationcoef
204
     */
205
    public $aggregationcoef = 0;
206
 
207
    /**
208
     * Aggregation coeficient used for weighted averages only
209
     * @var float $aggregationcoef2
210
     */
211
    public $aggregationcoef2 = 0;
212
 
213
    /**
214
     * Sorting order of the columns.
215
     * @var int $sortorder
216
     */
217
    public $sortorder = 0;
218
 
219
    /**
220
     * Display type of the grades (Real, Percentage, Letter, or default).
221
     * @var int $display
222
     */
223
    public $display = GRADE_DISPLAY_TYPE_DEFAULT;
224
 
225
    /**
226
     * The number of digits after the decimal point symbol. Applies only to REAL and PERCENTAGE grade display types.
227
     * @var int $decimals
228
     */
229
    public $decimals = null;
230
 
231
    /**
232
     * Grade item lock flag. Empty if not locked, locked if any value present, usually date when item was locked. Locking prevents updating.
233
     * @var int $locked
234
     */
235
    public $locked = 0;
236
 
237
    /**
238
     * Date after which the grade will be locked. Empty means no automatic locking.
239
     * @var int $locktime
240
     */
241
    public $locktime = 0;
242
 
243
    /**
244
     * If set, the whole column will be recalculated, then this flag will be switched off.
245
     * @var bool $needsupdate
246
     */
247
    public $needsupdate = 1;
248
 
249
    /**
250
     * If set, the grade item's weight has been overridden by a user and should not be automatically adjusted.
251
     */
252
    public $weightoverride = 0;
253
 
254
    /**
255
     * Cached dependson array
256
     * @var array An array of cached grade item dependencies.
257
     */
258
    public $dependson_cache = null;
259
 
260
    /**
261
     * @var bool If we regrade this item should we mark it as overridden?
262
     */
263
    public $markasoverriddenwhengraded = true;
264
 
265
    /**
266
     * @var int course module ID
267
     */
268
    public $cmid;
269
 
270
    /**
271
     * @var string average information.
272
     */
273
    public $avg;
274
 
275
    /**
276
     * Category name.
277
     * @var string
278
     */
279
    public $category;
280
 
281
    /**
282
     * Constructor. Optionally (and by default) attempts to fetch corresponding row from the database
283
     *
284
     * @param array $params An array with required parameters for this grade object.
285
     * @param bool $fetch Whether to fetch corresponding row from the database or not,
286
     *        optional fields might not be defined if false used
287
     */
288
    public function __construct($params = null, $fetch = true) {
289
        global $CFG;
290
        // Set grademax from $CFG->gradepointdefault .
291
        self::set_properties($this, array('grademax' => $CFG->gradepointdefault));
292
        parent::__construct($params, $fetch);
293
    }
294
 
295
    /**
296
     * In addition to update() as defined in grade_object, handle the grade_outcome and grade_scale objects.
297
     * Force regrading if necessary, rounds the float numbers using php function,
298
     * the reason is we need to compare the db value with computed number to skip regrading if possible.
299
     *
300
     * @param string $source from where was the object inserted (mod/forum, manual, etc.)
301
     * @param bool $isbulkupdate If bulk grade update is happening.
302
     * @return bool success
303
     */
304
    public function update($source = null, $isbulkupdate = false) {
305
        // reset caches
306
        $this->dependson_cache = null;
307
 
308
        // Retrieve scale and infer grademax/min from it if needed
309
        $this->load_scale();
310
 
311
        // make sure there is not 0 in outcomeid
312
        if (empty($this->outcomeid)) {
313
            $this->outcomeid = null;
314
        }
315
 
316
        if ($this->qualifies_for_regrading()) {
317
            $this->force_regrading();
318
        }
319
 
320
        $this->timemodified = time();
321
 
322
        $this->grademin        = grade_floatval($this->grademin);
323
        $this->grademax        = grade_floatval($this->grademax);
324
        $this->multfactor      = grade_floatval($this->multfactor);
325
        $this->plusfactor      = grade_floatval($this->plusfactor);
326
        $this->aggregationcoef = grade_floatval($this->aggregationcoef);
327
        $this->aggregationcoef2 = grade_floatval($this->aggregationcoef2);
328
 
329
        $result = parent::update($source, $isbulkupdate);
330
 
331
        if ($result) {
332
            $event = \core\event\grade_item_updated::create_from_grade_item($this);
333
            $event->trigger();
334
        }
335
 
336
        return $result;
337
    }
338
 
339
    /**
340
     * Compares the values held by this object with those of the matching record in DB, and returns
341
     * whether or not these differences are sufficient to justify an update of all parent objects.
342
     * This assumes that this object has an id number and a matching record in DB. If not, it will return false.
343
     *
344
     * @return bool
345
     */
346
    public function qualifies_for_regrading() {
347
        if (empty($this->id)) {
348
            return false;
349
        }
350
 
351
        $db_item = new grade_item(array('id' => $this->id));
352
 
353
        $calculationdiff = $db_item->calculation != $this->calculation;
354
        $categorydiff    = $db_item->categoryid  != $this->categoryid;
355
        $gradetypediff   = $db_item->gradetype   != $this->gradetype;
356
        $scaleiddiff     = $db_item->scaleid     != $this->scaleid;
357
        $outcomeiddiff   = $db_item->outcomeid   != $this->outcomeid;
358
        $locktimediff    = $db_item->locktime    != $this->locktime;
359
        $grademindiff    = grade_floats_different($db_item->grademin,        $this->grademin);
360
        $grademaxdiff    = grade_floats_different($db_item->grademax,        $this->grademax);
361
        $multfactordiff  = grade_floats_different($db_item->multfactor,      $this->multfactor);
362
        $plusfactordiff  = grade_floats_different($db_item->plusfactor,      $this->plusfactor);
363
        $acoefdiff       = grade_floats_different($db_item->aggregationcoef, $this->aggregationcoef);
364
        $acoefdiff2      = grade_floats_different($db_item->aggregationcoef2, $this->aggregationcoef2);
365
        $weightoverride  = grade_floats_different($db_item->weightoverride, $this->weightoverride);
366
 
367
        $needsupdatediff = !$db_item->needsupdate &&  $this->needsupdate;    // force regrading only if setting the flag first time
368
        $lockeddiff      = !empty($db_item->locked) && empty($this->locked); // force regrading only when unlocking
369
 
370
        return ($calculationdiff || $categorydiff || $gradetypediff || $grademaxdiff || $grademindiff || $scaleiddiff
371
             || $outcomeiddiff || $multfactordiff || $plusfactordiff || $needsupdatediff
372
             || $lockeddiff || $acoefdiff || $acoefdiff2 || $weightoverride || $locktimediff);
373
    }
374
 
375
    /**
376
     * Finds and returns a grade_item instance based on params.
377
     *
378
     * @static
379
     * @param array $params associative arrays varname=>value
380
     * @return grade_item|bool Returns a grade_item instance or false if none found
381
     */
382
    public static function fetch($params) {
383
        return grade_object::fetch_helper('grade_items', 'grade_item', $params);
384
    }
385
 
386
    /**
387
     * Check to see if there are any existing grades for this grade_item.
388
     *
389
     * @return boolean - true if there are valid grades for this grade_item.
390
     */
391
    public function has_grades() {
392
        global $DB;
393
 
394
        $count = $DB->count_records_select('grade_grades',
395
                                           'itemid = :gradeitemid AND finalgrade IS NOT NULL',
396
                                           array('gradeitemid' => $this->id));
397
        return $count > 0;
398
    }
399
 
400
    /**
401
     * Check to see if there are existing overridden grades for this grade_item.
402
     *
403
     * @return boolean - true if there are overridden grades for this grade_item.
404
     */
405
    public function has_overridden_grades() {
406
        global $DB;
407
 
408
        $count = $DB->count_records_select('grade_grades',
409
                                           'itemid = :gradeitemid AND finalgrade IS NOT NULL AND overridden > 0',
410
                                           array('gradeitemid' => $this->id));
411
        return $count > 0;
412
    }
413
 
414
    /**
415
     * Finds and returns all grade_item instances based on params.
416
     *
417
     * @static
418
     * @param array $params associative arrays varname=>value
419
     * @return array array of grade_item instances or false if none found.
420
     */
421
    public static function fetch_all($params) {
422
        return grade_object::fetch_all_helper('grade_items', 'grade_item', $params);
423
    }
424
 
425
    /**
426
     * Delete all grades and force_regrading of parent category.
427
     *
428
     * @param string $source from where was the object deleted (mod/forum, manual, etc.)
429
     * @return bool success
430
     */
431
    public function delete($source=null) {
432
        global $DB;
433
 
434
        try {
435
            $transaction = $DB->start_delegated_transaction();
436
            $this->delete_all_grades($source);
437
            $success = parent::delete($source);
438
            if ($success) {
439
                $event = \core\event\grade_item_deleted::create_from_grade_item($this);
440
                $event->trigger();
441
            }
442
            $transaction->allow_commit();
443
        } catch (Exception $e) {
444
            $transaction->rollback($e);
445
        }
446
        return $success;
447
    }
448
 
449
    /**
450
     * Delete all grades
451
     *
452
     * @param string $source from where was the object deleted (mod/forum, manual, etc.)
453
     * @return bool
454
     */
455
    public function delete_all_grades($source=null) {
456
        global $DB;
457
 
458
        try {
459
            $transaction = $DB->start_delegated_transaction();
460
 
461
            if (!$this->is_course_item()) {
462
                $this->force_regrading();
463
            }
464
 
465
            if ($grades = grade_grade::fetch_all(['itemid' => $this->id])) {
466
                foreach ($grades as $grade) {
467
                    $grade->delete($source);
468
                }
469
            }
470
 
471
            // Delete all the historical files.
472
            // We only support feedback files for modules atm.
473
            if ($this->is_external_item()) {
474
                $fs = new file_storage();
475
                $fs->delete_area_files($this->get_context()->id, GRADE_FILE_COMPONENT, GRADE_HISTORY_FEEDBACK_FILEAREA);
476
            }
477
 
478
            $transaction->allow_commit();
479
        } catch (Exception $e) {
480
            $transaction->rollback($e);
481
        }
482
        return true;
483
    }
484
 
485
    /**
486
     * Duplicate grade item.
487
     *
488
     * @return grade_item The duplicate grade item
489
     */
490
    public function duplicate() {
491
        // Convert current object to array.
492
        $copy = (array) $this;
493
 
494
        if (empty($copy["id"])) {
495
            throw new moodle_exception('invalidgradeitemid');
496
        }
497
 
498
        // Remove fields that will be either unique or automatically filled.
499
        $removekeys = array();
500
        $removekeys[] = 'id';
501
        $removekeys[] = 'idnumber';
502
        $removekeys[] = 'timecreated';
503
        $removekeys[] = 'sortorder';
504
        foreach ($removekeys as $key) {
505
            unset($copy[$key]);
506
        }
507
 
508
        // Addendum to name.
509
        $copy["itemname"] = get_string('duplicatedgradeitem', 'grades', $copy["itemname"]);
510
 
511
        // Create new grade item.
512
        $gradeitem = new grade_item($copy);
513
 
514
        // Insert grade item into database.
515
        $gradeitem->insert();
516
 
517
        return $gradeitem;
518
    }
519
 
520
    /**
521
     * In addition to perform parent::insert(), calls force_regrading() method too.
522
     *
523
     * @param string $source From where was the object inserted (mod/forum, manual, etc.)
524
     * @param string $isbulkupdate If bulk grade update is happening.
525
     * @return int PK ID if successful, false otherwise
526
     */
527
    public function insert($source = null, $isbulkupdate = false) {
528
        global $CFG, $DB;
529
 
530
        if (empty($this->courseid)) {
531
            throw new \moodle_exception('cannotinsertgrade');
532
        }
533
 
534
        // load scale if needed
535
        $this->load_scale();
536
 
537
        // add parent category if needed
538
        if (empty($this->categoryid) and !$this->is_course_item() and !$this->is_category_item()) {
539
            $course_category = grade_category::fetch_course_category($this->courseid);
540
            $this->categoryid = $course_category->id;
541
 
542
        }
543
 
544
        // always place the new items at the end, move them after insert if needed
545
        $last_sortorder = $DB->get_field_select('grade_items', 'MAX(sortorder)', "courseid = ?", array($this->courseid));
546
        if (!empty($last_sortorder)) {
547
            $this->sortorder = $last_sortorder + 1;
548
        } else {
549
            $this->sortorder = 1;
550
        }
551
 
552
        // add proper item numbers to manual items
553
        if ($this->itemtype == 'manual') {
554
            if (empty($this->itemnumber)) {
555
                $this->itemnumber = 0;
556
            }
557
        }
558
 
559
        // make sure there is not 0 in outcomeid
560
        if (empty($this->outcomeid)) {
561
            $this->outcomeid = null;
562
        }
563
 
564
        $this->timecreated = $this->timemodified = time();
565
 
566
        if (parent::insert($source, $isbulkupdate)) {
567
            // force regrading of items if needed
568
            $this->force_regrading();
569
 
570
            $event = \core\event\grade_item_created::create_from_grade_item($this);
571
            $event->trigger();
572
 
573
            return $this->id;
574
 
575
        } else {
576
            debugging("Could not insert this grade_item in the database!");
577
            return false;
578
        }
579
    }
580
 
581
    /**
582
     * Set idnumber of grade item, updates also course_modules table
583
     *
584
     * @param string $idnumber (without magic quotes)
585
     * @return bool success
586
     */
587
    public function add_idnumber($idnumber) {
588
        global $DB;
589
        if (!empty($this->idnumber)) {
590
            return false;
591
        }
592
 
593
        if ($this->itemtype == 'mod' and !$this->is_outcome_item()) {
594
            if ($this->itemnumber == 0) {
595
                // for activity modules, itemnumber 0 is synced with the course_modules
596
                if (!$cm = get_coursemodule_from_instance($this->itemmodule, $this->iteminstance, $this->courseid)) {
597
                    return false;
598
                }
599
                if (!empty($cm->idnumber)) {
600
                    return false;
601
                }
602
                $DB->set_field('course_modules', 'idnumber', $idnumber, array('id' => $cm->id));
603
                $this->idnumber = $idnumber;
604
                return $this->update();
605
            } else {
606
                $this->idnumber = $idnumber;
607
                return $this->update();
608
            }
609
 
610
        } else {
611
            $this->idnumber = $idnumber;
612
            return $this->update();
613
        }
614
    }
615
 
616
    /**
617
     * Returns the locked state of this grade_item (if the grade_item is locked OR no specific
618
     * $userid is given) or the locked state of a specific grade within this item if a specific
619
     * $userid is given and the grade_item is unlocked.
620
     *
621
     * @param int $userid The user's ID
622
     * @return bool Locked state
623
     */
624
    public function is_locked($userid=NULL) {
625
        global $CFG;
626
 
627
        // Override for any grade items belonging to activities which are in the process of being deleted.
628
        require_once($CFG->dirroot . '/course/lib.php');
629
        if (course_module_instance_pending_deletion($this->courseid, $this->itemmodule, $this->iteminstance)) {
630
            return true;
631
        }
632
 
633
        if (!empty($this->locked)) {
634
            return true;
635
        }
636
 
637
        if (!empty($userid)) {
638
            if ($grade = grade_grade::fetch(array('itemid'=>$this->id, 'userid'=>$userid))) {
639
                $grade->grade_item =& $this; // prevent db fetching of cached grade_item
640
                return $grade->is_locked();
641
            }
642
        }
643
 
644
        return false;
645
    }
646
 
647
    /**
648
     * Locks or unlocks this grade_item and (optionally) all its associated final grades.
649
     *
650
     * @param int $lockedstate 0, 1 or a timestamp int(10) after which date the item will be locked.
651
     * @param bool $cascade Lock/unlock child objects too
652
     * @param bool $refresh Refresh grades when unlocking
653
     * @return bool True if grade_item all grades updated, false if at least one update fails
654
     */
655
    public function set_locked($lockedstate, $cascade=false, $refresh=true) {
656
        if ($lockedstate) {
657
            // Setting lock.
658
            if (empty($this->id)) {
659
                return false;
660
            } else if ($this->needsupdate) {
661
                // Can not lock grade without first having final grade,
662
                // so we schedule it to be locked as soon as regrading is finished.
663
                $this->locktime = time() - 1;
664
            } else {
665
                $this->locked = time();
666
            }
667
            $this->update();
668
 
669
            if ($cascade) {
670
                $grades = $this->get_final();
671
                foreach($grades as $g) {
672
                    $grade = new grade_grade($g, false);
673
                    $grade->grade_item =& $this;
674
                    $grade->set_locked(1, null, false);
675
                }
676
            }
677
 
678
            return true;
679
 
680
        } else {
681
        /// removing lock
682
            if (!empty($this->locked) and $this->locktime < time()) {
683
                //we have to reset locktime or else it would lock up again
684
                $this->locktime = 0;
685
            }
686
 
687
            $this->locked = 0;
688
            $this->update();
689
 
690
            if ($cascade) {
691
                if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
692
                    foreach($grades as $grade) {
693
                        $grade->grade_item =& $this;
694
                        $grade->set_locked(0, null, false);
695
                    }
696
                }
697
            }
698
 
699
            if ($refresh) {
700
                //refresh when unlocking
701
                $this->refresh_grades();
702
            }
703
 
704
            return true;
705
        }
706
    }
707
 
708
    /**
709
     * Lock the grade if needed. Make sure this is called only when final grades are valid
710
     */
711
    public function check_locktime() {
712
        if (!empty($this->locked)) {
713
            return; // already locked
714
        }
715
 
716
        if ($this->locktime and $this->locktime < time()) {
717
            $this->locked = time();
718
            $this->update('locktime');
719
        }
720
    }
721
 
722
    /**
723
     * Set the locktime for this grade item.
724
     *
725
     * @param int $locktime timestamp for lock to activate
726
     * @return void
727
     */
728
    public function set_locktime($locktime) {
729
        $this->locktime = $locktime;
730
        $this->update();
731
    }
732
 
733
    /**
734
     * Set the locktime for this grade item.
735
     *
736
     * @return int $locktime timestamp for lock to activate
737
     */
738
    public function get_locktime() {
739
        return $this->locktime;
740
    }
741
 
742
    /**
743
     * Set the hidden status of grade_item and all grades.
744
     *
745
     * 0 mean always visible, 1 means always hidden and a number > 1 is a timestamp to hide until
746
     *
747
     * @param int $hidden new hidden status
748
     * @param bool $cascade apply to child objects too
749
     */
750
    public function set_hidden($hidden, $cascade=false) {
751
        parent::set_hidden($hidden, $cascade);
752
 
753
        if ($cascade) {
754
            if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
755
                foreach($grades as $grade) {
756
                    $grade->grade_item =& $this;
757
                    $grade->set_hidden($hidden, $cascade);
758
                }
759
            }
760
        }
761
 
762
        //if marking item visible make sure category is visible MDL-21367
763
        if( !$hidden ) {
764
            $category_array = grade_category::fetch_all(array('id'=>$this->categoryid));
765
            if ($category_array && array_key_exists($this->categoryid, $category_array)) {
766
                $category = $category_array[$this->categoryid];
767
                //call set_hidden on the category regardless of whether it is hidden as its parent might be hidden
768
                $category->set_hidden($hidden, false);
769
            }
770
        }
771
    }
772
 
773
    /**
774
     * Returns the number of grades that are hidden
775
     *
776
     * @param string $groupsql SQL to limit the query by group
777
     * @param array $params SQL params for $groupsql
778
     * @param string $groupwheresql Where conditions for $groupsql
779
     * @return int The number of hidden grades
780
     */
781
    public function has_hidden_grades($groupsql="", array $params=null, $groupwheresql="") {
782
        global $DB;
783
        $params = (array)$params;
784
        $params['itemid'] = $this->id;
785
 
786
        return $DB->get_field_sql("SELECT COUNT(*) FROM {grade_grades} g LEFT JOIN "
787
                            ."{user} u ON g.userid = u.id $groupsql WHERE itemid = :itemid AND hidden = 1 $groupwheresql", $params);
788
    }
789
 
790
    /**
791
     * Mark regrading as finished successfully. This will also be called when subsequent regrading will not change any grades.
792
     * Situations such as an error being found will still result in the regrading being finished.
793
     */
794
    public function regrading_finished() {
795
        global $DB;
796
        $this->needsupdate = 0;
797
        //do not use $this->update() because we do not want this logged in grade_item_history
798
        $DB->set_field('grade_items', 'needsupdate', 0, array('id' => $this->id));
799
    }
800
 
801
    /**
802
     * Performs the necessary calculations on the grades_final referenced by this grade_item.
803
     * Also resets the needsupdate flag once successfully performed.
804
     *
805
     * This function must be used ONLY from lib/gradeslib.php/grade_regrade_final_grades(),
806
     * because the regrading must be done in correct order!!
807
     *
808
     * @param int $userid Supply a user ID to limit the regrading to a single user
809
     * @param \core\progress\base|null $progress Optional progress object, will be updated per user
810
     * @return bool true if ok, error string otherwise
811
     */
812
    public function regrade_final_grades($userid=null, ?\core\progress\base $progress = null) {
813
        global $CFG, $DB;
814
 
815
        // locked grade items already have correct final grades
816
        if ($this->is_locked()) {
817
            return true;
818
        }
819
 
820
        // calculation produces final value using formula from other final values
821
        if ($this->is_calculated()) {
822
            if ($this->compute($userid)) {
823
                return true;
824
            } else {
825
                return "Could not calculate grades for grade item"; // TODO: improve and localize
826
            }
827
 
828
        // noncalculated outcomes already have final values - raw grades not used
829
        } else if ($this->is_outcome_item()) {
830
            return true;
831
 
832
        // aggregate the category grade
833
        } else if ($this->is_category_item() or $this->is_course_item()) {
834
            // aggregate category grade item
835
            $category = $this->load_item_category();
836
            $category->grade_item =& $this;
837
            if ($category->generate_grades($userid, $progress)) {
838
                return true;
839
            } else {
840
                return "Could not aggregate final grades for category:".$this->id; // TODO: improve and localize
841
            }
842
 
843
        } else if ($this->is_manual_item()) {
844
            // manual items track only final grades, no raw grades
845
            return true;
846
 
847
        } else if (!$this->is_raw_used()) {
848
            // hmm - raw grades are not used- nothing to regrade
849
            return true;
850
        }
851
 
852
        // normal grade item - just new final grades
853
        $result = true;
854
        $grade_inst = new grade_grade();
855
        $fields = implode(',', $grade_inst->required_fields);
856
        if ($userid) {
857
            $params = array($this->id, $userid);
858
            $rs = $DB->get_recordset_select('grade_grades', "itemid=? AND userid=?", $params, '', $fields);
859
        } else {
860
            $rs = $DB->get_recordset('grade_grades', array('itemid' => $this->id), '', $fields);
861
        }
862
        if ($rs) {
863
            foreach ($rs as $grade_record) {
864
                $grade = new grade_grade($grade_record, false);
865
 
866
                // Incrementing the progress by nothing causes it to send an update (once per second)
867
                // to the web browser so as to prevent the connection timing out.
868
                if ($progress) {
869
                    $progress->increment_progress(0);
870
                }
871
 
872
                if (!empty($grade_record->locked) or !empty($grade_record->overridden)) {
873
                    // this grade is locked - final grade must be ok
874
                    continue;
875
                }
876
 
877
                $grade->finalgrade = $this->adjust_raw_grade($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
878
 
879
                if (grade_floats_different($grade_record->finalgrade, $grade->finalgrade)) {
880
                    $success = $grade->update('system');
881
 
882
                    // If successful trigger a user_graded event.
883
                    if ($success) {
884
                        $grade->load_grade_item();
885
                        \core\event\user_graded::create_from_grade($grade, \core\event\base::USER_OTHER)->trigger();
886
                    } else {
887
                        $result = "Internal error updating final grade";
888
                    }
889
                }
890
            }
891
            $rs->close();
892
        }
893
 
894
        return $result;
895
    }
896
 
897
    /**
898
     * Given a float grade value or integer grade scale, applies a number of adjustment based on
899
     * grade_item variables and returns the result.
900
     *
901
     * @param float $rawgrade The raw grade value
902
     * @param float $rawmin original rawmin
903
     * @param float $rawmax original rawmax
904
     * @return mixed
905
     */
906
    public function adjust_raw_grade($rawgrade, $rawmin, $rawmax) {
907
        if (is_null($rawgrade)) {
908
            return null;
909
        }
910
 
911
        if ($this->gradetype == GRADE_TYPE_VALUE) { // Dealing with numerical grade
912
 
913
            if ($this->grademax < $this->grademin) {
914
                return null;
915
            }
916
 
917
            if ($this->grademax == $this->grademin) {
918
                return $this->grademax; // no range
919
            }
920
 
921
            // Standardise score to the new grade range
922
            // NOTE: skip if the activity provides a manual rescaling option.
923
            $manuallyrescale = (component_callback_exists('mod_' . $this->itemmodule, 'rescale_activity_grades') !== false);
924
            if (!$manuallyrescale && ($rawmin != $this->grademin or $rawmax != $this->grademax)) {
925
                $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax);
926
            }
927
 
928
            // Apply other grade_item factors
929
            $rawgrade *= $this->multfactor;
930
            $rawgrade += $this->plusfactor;
931
 
932
            return $this->bounded_grade($rawgrade);
933
 
934
        } else if ($this->gradetype == GRADE_TYPE_SCALE) { // Dealing with a scale value
935
            if (empty($this->scale)) {
936
                $this->load_scale();
937
            }
938
 
939
            if ($this->grademax < 0) {
940
                return null; // scale not present - no grade
941
            }
942
 
943
            if ($this->grademax == 0) {
944
                return $this->grademax; // only one option
945
            }
946
 
947
            // Convert scale if needed
948
            // NOTE: skip if the activity provides a manual rescaling option.
949
            $manuallyrescale = (component_callback_exists('mod_' . $this->itemmodule, 'rescale_activity_grades') !== false);
950
            if (!$manuallyrescale && ($rawmin != $this->grademin or $rawmax != $this->grademax)) {
951
                // This should never happen because scales are locked if they are in use.
952
                $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax);
953
            }
954
 
955
            return $this->bounded_grade($rawgrade);
956
 
957
 
958
        } else if ($this->gradetype == GRADE_TYPE_TEXT or $this->gradetype == GRADE_TYPE_NONE) { // no value
959
            // somebody changed the grading type when grades already existed
960
            return null;
961
 
962
        } else {
963
            debugging("Unknown grade type");
964
            return null;
965
        }
966
    }
967
 
968
    /**
969
     * Update the rawgrademax and rawgrademin for all grade_grades records for this item.
970
     * Scale every rawgrade to maintain the percentage. This function should be called
971
     * after the gradeitem has been updated to the new min and max values.
972
     *
973
     * @param float $oldgrademin The previous grade min value
974
     * @param float $oldgrademax The previous grade max value
975
     * @param float $newgrademin The new grade min value
976
     * @param float $newgrademax The new grade max value
977
     * @param string $source from where was the object inserted (mod/forum, manual, etc.)
978
     * @return bool True on success
979
     */
980
    public function rescale_grades_keep_percentage($oldgrademin, $oldgrademax, $newgrademin, $newgrademax, $source = null) {
981
        global $DB;
982
 
983
        if (empty($this->id)) {
984
            return false;
985
        }
986
 
987
        if ($oldgrademax <= $oldgrademin) {
988
            // Grades cannot be scaled.
989
            return false;
990
        }
991
        $scale = ($newgrademax - $newgrademin) / ($oldgrademax - $oldgrademin);
992
        if (($newgrademax - $newgrademin) <= 1) {
993
            // We would lose too much precision, lets bail.
994
            return false;
995
        }
996
 
997
        $rs = $DB->get_recordset('grade_grades', array('itemid' => $this->id));
998
 
999
        foreach ($rs as $graderecord) {
1000
            // For each record, create an object to work on.
1001
            $grade = new grade_grade($graderecord, false);
1002
            // Set this object in the item so it doesn't re-fetch it.
1003
            $grade->grade_item = $this;
1004
 
1005
            if (!$this->is_category_item() || ($this->is_category_item() && $grade->is_overridden())) {
1006
                // Updating the raw grade automatically updates the min/max.
1007
                if ($this->is_raw_used()) {
1008
                    $rawgrade = (($grade->rawgrade - $oldgrademin) * $scale) + $newgrademin;
1009
                    $this->update_raw_grade(false, $rawgrade, $source, false, FORMAT_MOODLE, null, null, null, $grade);
1010
                } else {
1011
                    $finalgrade = (($grade->finalgrade - $oldgrademin) * $scale) + $newgrademin;
1012
                    $this->update_final_grade($grade->userid, $finalgrade, $source);
1013
                }
1014
            }
1015
        }
1016
        $rs->close();
1017
 
1018
        // Mark this item for regrading.
1019
        $this->force_regrading();
1020
 
1021
        return true;
1022
    }
1023
 
1024
    /**
1025
     * Sets this grade_item's needsupdate to true. Also marks the course item as needing update.
1026
     *
1027
     * @return void
1028
     */
1029
    public function force_regrading() {
1030
        global $DB;
1031
        $this->needsupdate = 1;
1032
        //mark this item and course item only - categories and calculated items are always regraded
1033
        $wheresql = "(itemtype='course' OR id=?) AND courseid=?";
1034
        $params   = array($this->id, $this->courseid);
1035
        $DB->set_field_select('grade_items', 'needsupdate', 1, $wheresql, $params);
1036
    }
1037
 
1038
    /**
1039
     * Instantiates a grade_scale object from the DB if this item's scaleid variable is set
1040
     *
1041
     * @return grade_scale Returns a grade_scale object or null if no scale used
1042
     */
1043
    public function load_scale() {
1044
        if ($this->gradetype != GRADE_TYPE_SCALE) {
1045
            $this->scaleid = null;
1046
        }
1047
 
1048
        if (!empty($this->scaleid)) {
1049
            //do not load scale if already present
1050
            if (empty($this->scale->id) or $this->scale->id != $this->scaleid) {
1051
                $this->scale = grade_scale::fetch(array('id'=>$this->scaleid));
1052
                if (!$this->scale) {
1053
                    debugging('Incorrect scale id: '.$this->scaleid);
1054
                    $this->scale = null;
1055
                    return null;
1056
                }
1057
                $this->scale->load_items();
1058
            }
1059
 
1060
            // Until scales are uniformly set to min=0 max=count(scaleitems)-1 throughout Moodle, we
1061
            // stay with the current min=1 max=count(scaleitems)
1062
            $this->grademax = count($this->scale->scale_items);
1063
            $this->grademin = 1;
1064
 
1065
        } else {
1066
            $this->scale = null;
1067
        }
1068
 
1069
        return $this->scale;
1070
    }
1071
 
1072
    /**
1073
     * Instantiates a grade_outcome object from the DB if this item's outcomeid variable is set
1074
     *
1075
     * @return grade_outcome This grade item's associated grade_outcome or null
1076
     */
1077
    public function load_outcome() {
1078
        if (!empty($this->outcomeid)) {
1079
            $this->outcome = grade_outcome::fetch(array('id'=>$this->outcomeid));
1080
        }
1081
        return $this->outcome;
1082
    }
1083
 
1084
    /**
1085
     * Returns the grade_category object this grade_item belongs to (referenced by categoryid)
1086
     * or category attached to category item.
1087
     *
1088
     * @return grade_category|bool Returns a grade_category object if applicable or false if this is a course item
1089
     */
1090
    public function get_parent_category() {
1091
        if ($this->is_category_item() or $this->is_course_item()) {
1092
            return $this->get_item_category();
1093
 
1094
        } else {
1095
            return grade_category::fetch(array('id'=>$this->categoryid));
1096
        }
1097
    }
1098
 
1099
    /**
1100
     * Calls upon the get_parent_category method to retrieve the grade_category object
1101
     * from the DB and assigns it to $this->parent_category. It also returns the object.
1102
     *
1103
     * @return grade_category This grade item's parent grade_category.
1104
     */
1105
    public function load_parent_category() {
1106
        if (empty($this->parent_category->id)) {
1107
            $this->parent_category = $this->get_parent_category();
1108
        }
1109
        return $this->parent_category;
1110
    }
1111
 
1112
    /**
1113
     * Returns the grade_category for a grade category grade item
1114
     *
1115
     * @return grade_category|bool Returns a grade_category instance if applicable or false otherwise
1116
     */
1117
    public function get_item_category() {
1118
        if (!$this->is_course_item() and !$this->is_category_item()) {
1119
            return false;
1120
        }
1121
        return grade_category::fetch(array('id'=>$this->iteminstance));
1122
    }
1123
 
1124
    /**
1125
     * Calls upon the get_item_category method to retrieve the grade_category object
1126
     * from the DB and assigns it to $this->item_category. It also returns the object.
1127
     *
1128
     * @return grade_category
1129
     */
1130
    public function load_item_category() {
1131
        if (empty($this->item_category->id)) {
1132
            $this->item_category = $this->get_item_category();
1133
        }
1134
        return $this->item_category;
1135
    }
1136
 
1137
    /**
1138
     * Is the grade item associated with category?
1139
     *
1140
     * @return bool
1141
     */
1142
    public function is_category_item() {
1143
        return ($this->itemtype == 'category');
1144
    }
1145
 
1146
    /**
1147
     * Is the grade item associated with course?
1148
     *
1149
     * @return bool
1150
     */
1151
    public function is_course_item() {
1152
        return ($this->itemtype == 'course');
1153
    }
1154
 
1155
    /**
1156
     * Is this a manually graded item?
1157
     *
1158
     * @return bool
1159
     */
1160
    public function is_manual_item() {
1161
        return ($this->itemtype == 'manual');
1162
    }
1163
 
1164
    /**
1165
     * Is this an outcome item?
1166
     *
1167
     * @return bool
1168
     */
1169
    public function is_outcome_item() {
1170
        return !empty($this->outcomeid);
1171
    }
1172
 
1173
    /**
1174
     * Is the grade item external - associated with module, plugin or something else?
1175
     *
1176
     * @return bool
1177
     */
1178
    public function is_external_item() {
1179
        return ($this->itemtype == 'mod');
1180
    }
1181
 
1182
    /**
1183
     * Is the grade item overridable
1184
     *
1185
     * @return bool
1186
     */
1187
    public function is_overridable_item() {
1188
        if ($this->is_course_item() or $this->is_category_item()) {
1189
            $overridable = (bool) get_config('moodle', 'grade_overridecat');
1190
        } else {
1191
            $overridable = false;
1192
        }
1193
 
1194
        return !$this->is_outcome_item() and ($this->is_external_item() or $this->is_calculated() or $overridable);
1195
    }
1196
 
1197
    /**
1198
     * Is the grade item feedback overridable
1199
     *
1200
     * @return bool
1201
     */
1202
    public function is_overridable_item_feedback() {
1203
        return !$this->is_outcome_item() and $this->is_external_item();
1204
    }
1205
 
1206
    /**
1207
     * Returns true if grade items uses raw grades
1208
     *
1209
     * @return bool
1210
     */
1211
    public function is_raw_used() {
1212
        return ($this->is_external_item() and !$this->is_calculated() and !$this->is_outcome_item());
1213
    }
1214
 
1215
    /**
1216
     * Returns true if the grade item is an aggreggated type grade.
1217
     *
1218
     * @since  Moodle 2.8.7, 2.9.1
1219
     * @return bool
1220
     */
1221
    public function is_aggregate_item() {
1222
        return ($this->is_category_item() || $this->is_course_item());
1223
    }
1224
 
1225
    /**
1226
     * Returns the grade item associated with the course
1227
     *
1228
     * @param int $courseid
1229
     * @return grade_item Course level grade item object
1230
     */
1231
    public static function fetch_course_item($courseid) {
1232
        if ($course_item = grade_item::fetch(array('courseid'=>$courseid, 'itemtype'=>'course'))) {
1233
            return $course_item;
1234
        }
1235
 
1236
        // first get category - it creates the associated grade item
1237
        $course_category = grade_category::fetch_course_category($courseid);
1238
        return $course_category->get_grade_item();
1239
    }
1240
 
1241
    /**
1242
     * Is grading object editable?
1243
     *
1244
     * @return bool
1245
     */
1246
    public function is_editable() {
1247
        return true;
1248
    }
1249
 
1250
    /**
1251
     * Checks if grade calculated. Returns this object's calculation.
1252
     *
1253
     * @return bool true if grade item calculated.
1254
     */
1255
    public function is_calculated() {
1256
        if (empty($this->calculation)) {
1257
            return false;
1258
        }
1259
 
1260
        /*
1261
         * The main reason why we use the ##gixxx## instead of [[idnumber]] is speed of depends_on(),
1262
         * we would have to fetch all course grade items to find out the ids.
1263
         * Also if user changes the idnumber the formula does not need to be updated.
1264
         */
1265
 
1266
        // first detect if we need to change calculation formula from [[idnumber]] to ##giXXX## (after backup, etc.)
1267
        if (!$this->calculation_normalized and strpos($this->calculation, '[[') !== false) {
1268
            $this->set_calculation($this->calculation);
1269
        }
1270
 
1271
        return !empty($this->calculation);
1272
    }
1273
 
1274
    /**
1275
     * Returns calculation string if grade calculated.
1276
     *
1277
     * @return string Returns the grade item's calculation if calculation is used, null if not
1278
     */
1279
    public function get_calculation() {
1280
        if ($this->is_calculated()) {
1281
            return grade_item::denormalize_formula($this->calculation, $this->courseid);
1282
 
1283
        } else {
1284
            return NULL;
1285
        }
1286
    }
1287
 
1288
    /**
1289
     * Sets this item's calculation (creates it) if not yet set, or
1290
     * updates it if already set (in the DB). If no calculation is given,
1291
     * the calculation is removed.
1292
     *
1293
     * @param string $formula string representation of formula used for calculation
1294
     * @return bool success
1295
     */
1296
    public function set_calculation($formula) {
1297
        $this->calculation = grade_item::normalize_formula($formula, $this->courseid);
1298
        $this->calculation_normalized = true;
1299
        return $this->update();
1300
    }
1301
 
1302
    /**
1303
     * Denormalizes the calculation formula to [idnumber] form
1304
     *
1305
     * @param string $formula A string representation of the formula
1306
     * @param int $courseid The course ID
1307
     * @return string The denormalized formula as a string
1308
     */
1309
    public static function denormalize_formula($formula, $courseid) {
1310
        if (empty($formula)) {
1311
            return '';
1312
        }
1313
 
1314
        // denormalize formula - convert ##giXX## to [[idnumber]]
1315
        if (preg_match_all('/##gi(\d+)##/', $formula, $matches)) {
1316
            foreach ($matches[1] as $id) {
1317
                if ($grade_item = grade_item::fetch(array('id'=>$id, 'courseid'=>$courseid))) {
1318
                    if (!empty($grade_item->idnumber)) {
1319
                        $formula = str_replace('##gi'.$grade_item->id.'##', '[['.$grade_item->idnumber.']]', $formula);
1320
                    }
1321
                }
1322
            }
1323
        }
1324
 
1325
        return $formula;
1326
 
1327
    }
1328
 
1329
    /**
1330
     * Normalizes the calculation formula to [#giXX#] form
1331
     *
1332
     * @param string $formula The formula
1333
     * @param int $courseid The course ID
1334
     * @return string The normalized formula as a string
1335
     */
1336
    public static function normalize_formula($formula, $courseid) {
1337
        $formula = trim($formula);
1338
 
1339
        if (empty($formula)) {
1340
            return NULL;
1341
 
1342
        }
1343
 
1344
        // normalize formula - we want grade item ids ##giXXX## instead of [[idnumber]]
1345
        if ($grade_items = grade_item::fetch_all(array('courseid'=>$courseid))) {
1346
            foreach ($grade_items as $grade_item) {
1347
                $formula = str_replace('[['.$grade_item->idnumber.']]', '##gi'.$grade_item->id.'##', $formula);
1348
            }
1349
        }
1350
 
1351
        return $formula;
1352
    }
1353
 
1354
    /**
1355
     * Returns the final values for this grade item (as imported by module or other source).
1356
     *
1357
     * @param int $userid Optional: to retrieve a single user's final grade
1358
     * @return array|grade_grade An array of all grade_grade instances for this grade_item, or a single grade_grade instance.
1359
     */
1360
    public function get_final($userid=NULL) {
1361
        global $DB;
1362
        if ($userid) {
1363
            if ($user = $DB->get_record('grade_grades', array('itemid' => $this->id, 'userid' => $userid))) {
1364
                return $user;
1365
            }
1366
 
1367
        } else {
1368
            if ($grades = $DB->get_records('grade_grades', array('itemid' => $this->id))) {
1369
                //TODO: speed up with better SQL (MDL-31380)
1370
                $result = array();
1371
                foreach ($grades as $grade) {
1372
                    $result[$grade->userid] = $grade;
1373
                }
1374
                return $result;
1375
            } else {
1376
                return array();
1377
            }
1378
        }
1379
    }
1380
 
1381
    /**
1382
     * Get (or create if not exist yet) grade for this user
1383
     *
1384
     * @param int $userid The user ID
1385
     * @param bool $create If true and the user has no grade for this grade item a new grade_grade instance will be inserted
1386
     * @return grade_grade The grade_grade instance for the user for this grade item
1387
     */
1388
    public function get_grade($userid, $create=true) {
1389
        if (empty($this->id)) {
1390
            debugging('Can not use before insert');
1391
            return false;
1392
        }
1393
 
1394
        $grade = new grade_grade(array('userid'=>$userid, 'itemid'=>$this->id));
1395
        if (empty($grade->id) and $create) {
1396
            $grade->insert();
1397
        }
1398
 
1399
        return $grade;
1400
    }
1401
 
1402
    /**
1403
     * Returns the sortorder of this grade_item. This method is also available in
1404
     * grade_category, for cases where the object type is not know.
1405
     *
1406
     * @return int Sort order
1407
     */
1408
    public function get_sortorder() {
1409
        return $this->sortorder;
1410
    }
1411
 
1412
    /**
1413
     * Returns the idnumber of this grade_item. This method is also available in
1414
     * grade_category, for cases where the object type is not know.
1415
     *
1416
     * @return string The grade item idnumber
1417
     */
1418
    public function get_idnumber() {
1419
        return $this->idnumber;
1420
    }
1421
 
1422
    /**
1423
     * Returns this grade_item. This method is also available in
1424
     * grade_category, for cases where the object type is not know.
1425
     *
1426
     * @return grade_item
1427
     */
1428
    public function get_grade_item() {
1429
        return $this;
1430
    }
1431
 
1432
    /**
1433
     * Sets the sortorder of this grade_item. This method is also available in
1434
     * grade_category, for cases where the object type is not know.
1435
     *
1436
     * @param int $sortorder
1437
     */
1438
    public function set_sortorder($sortorder) {
1439
        if ($this->sortorder == $sortorder) {
1440
            return;
1441
        }
1442
        $this->sortorder = $sortorder;
1443
        $this->update();
1444
    }
1445
 
1446
    /**
1447
     * Update this grade item's sortorder so that it will appear after $sortorder
1448
     *
1449
     * @param int $sortorder The sort order to place this grade item after
1450
     */
1451
    public function move_after_sortorder($sortorder) {
1452
        global $CFG, $DB;
1453
 
1454
        //make some room first
1455
        $params = array($sortorder, $this->courseid);
1456
        $sql = "UPDATE {grade_items}
1457
                   SET sortorder = sortorder + 1
1458
                 WHERE sortorder > ? AND courseid = ?";
1459
        $DB->execute($sql, $params);
1460
 
1461
        $this->set_sortorder($sortorder + 1);
1462
    }
1463
 
1464
    /**
1465
     * Detect duplicate grade item's sortorder and re-sort them.
1466
     * Note: Duplicate sortorder will be introduced while duplicating activities or
1467
     * merging two courses.
1468
     *
1469
     * @param int $courseid id of the course for which grade_items sortorder need to be fixed.
1470
     */
1471
    public static function fix_duplicate_sortorder($courseid) {
1472
        global $DB;
1473
 
1474
        $transaction = $DB->start_delegated_transaction();
1475
 
1476
        $sql = "SELECT DISTINCT g1.id, g1.courseid, g1.sortorder
1477
                    FROM {grade_items} g1
1478
                    JOIN {grade_items} g2 ON g1.courseid = g2.courseid
1479
                WHERE g1.sortorder = g2.sortorder AND g1.id != g2.id AND g1.courseid = :courseid
1480
                ORDER BY g1.sortorder DESC, g1.id DESC";
1481
 
1482
        // Get all duplicates in course highest sort order, and higest id first so that we can make space at the
1483
        // bottom higher end of the sort orders and work down by id.
1484
        $rs = $DB->get_recordset_sql($sql, array('courseid' => $courseid));
1485
 
1486
        foreach($rs as $duplicate) {
1487
            $DB->execute("UPDATE {grade_items}
1488
                            SET sortorder = sortorder + 1
1489
                          WHERE courseid = :courseid AND
1490
                          (sortorder > :sortorder OR (sortorder = :sortorder2 AND id > :id))",
1491
                array('courseid' => $duplicate->courseid,
1492
                    'sortorder' => $duplicate->sortorder,
1493
                    'sortorder2' => $duplicate->sortorder,
1494
                    'id' => $duplicate->id));
1495
        }
1496
        $rs->close();
1497
        $transaction->allow_commit();
1498
    }
1499
 
1500
    /**
1501
     * Returns the most descriptive field for this object.
1502
     *
1503
     * Determines what type of grade item it is then returns the appropriate string
1504
     *
1505
     * @param bool $fulltotal If the item is a category total, returns $categoryname."total" instead of "Category total" or "Course total"
1506
     * @param bool $escape Whether the returned category name is to be HTML escaped or not.
1507
     * @return string name
1508
     */
1509
    public function get_name($fulltotal=false, $escape = true) {
1510
        global $CFG;
1511
        require_once($CFG->dirroot . '/course/lib.php');
1512
        if (strval($this->itemname) !== '') {
1513
            // MDL-10557
1514
 
1515
            // Make it obvious to users if the course module to which this grade item relates, is currently being removed.
1516
            $deletionpending = course_module_instance_pending_deletion($this->courseid, $this->itemmodule, $this->iteminstance);
1517
            $deletionnotice = get_string('gradesmoduledeletionprefix', 'grades');
1518
 
1519
            $options = ['context' => context_course::instance($this->courseid), 'escape' => $escape];
1520
            return $deletionpending ?
1521
                format_string($deletionnotice . ' ' . $this->itemname, true, $options) :
1522
                format_string($this->itemname, true, $options);
1523
 
1524
        } else if ($this->is_course_item()) {
1525
            return get_string('coursetotal', 'grades');
1526
 
1527
        } else if ($this->is_category_item()) {
1528
            if ($fulltotal) {
1529
                $category = $this->load_parent_category();
1530
                $a = new stdClass();
1531
                $a->category = $category->get_name($escape);
1532
                return get_string('categorytotalfull', 'grades', $a);
1533
            } else {
1534
            return get_string('categorytotal', 'grades');
1535
            }
1536
 
1537
        } else {
1538
            return get_string('gradenoun');
1539
        }
1540
    }
1541
 
1542
    /**
1543
     * A grade item can return a more detailed description which will be added to the header of the column/row in some reports.
1544
     *
1545
     * @return string description
1546
     */
1547
    public function get_description() {
1548
        if ($this->is_course_item() || $this->is_category_item()) {
1549
            $categoryitem = $this->load_item_category();
1550
            return $categoryitem->get_description();
1551
        }
1552
        return '';
1553
    }
1554
 
1555
    /**
1556
     * Sets this item's categoryid. A generic method shared by objects that have a parent id of some kind.
1557
     *
1558
     * @param int $parentid The ID of the new parent
1559
     * @param bool $updateaggregationfields Whether or not to convert the aggregation fields when switching between category.
1560
     *                          Set this to false when the aggregation fields have been updated in prevision of the new
1561
     *                          category, typically when the item is freshly created.
1562
     * @return bool True if success
1563
     */
1564
    public function set_parent($parentid, $updateaggregationfields = true) {
1565
        if ($this->is_course_item() or $this->is_category_item()) {
1566
            throw new \moodle_exception('cannotsetparentforcatoritem');
1567
        }
1568
 
1569
        if ($this->categoryid == $parentid) {
1570
            return true;
1571
        }
1572
 
1573
        // find parent and check course id
1574
        if (!$parent_category = grade_category::fetch(array('id'=>$parentid, 'courseid'=>$this->courseid))) {
1575
            return false;
1576
        }
1577
 
1578
        $currentparent = $this->load_parent_category();
1579
 
1580
        if ($updateaggregationfields) {
1581
            $this->set_aggregation_fields_for_aggregation($currentparent->aggregation, $parent_category->aggregation);
1582
        }
1583
 
1584
        $this->force_regrading();
1585
 
1586
        // set new parent
1587
        $this->categoryid = $parent_category->id;
1588
        $this->parent_category =& $parent_category;
1589
 
1590
        return $this->update();
1591
    }
1592
 
1593
    /**
1594
     * Update the aggregation fields when the aggregation changed.
1595
     *
1596
     * This method should always be called when the aggregation has changed, but also when
1597
     * the item was moved to another category, even it if uses the same aggregation method.
1598
     *
1599
     * Some values such as the weight only make sense within a category, once moved the
1600
     * values should be reset to let the user adapt them accordingly.
1601
     *
1602
     * Note that this method does not save the grade item.
1603
     * {@link grade_item::update()} has to be called manually after using this method.
1604
     *
1605
     * @param  int $from Aggregation method constant value.
1606
     * @param  int $to   Aggregation method constant value.
1607
     * @return boolean   True when at least one field was changed, false otherwise
1608
     */
1609
    public function set_aggregation_fields_for_aggregation($from, $to) {
1610
        $defaults = grade_category::get_default_aggregation_coefficient_values($to);
1611
 
1612
        $origaggregationcoef = $this->aggregationcoef;
1613
        $origaggregationcoef2 = $this->aggregationcoef2;
1614
        $origweighoverride = $this->weightoverride;
1615
 
1616
        if ($from == GRADE_AGGREGATE_SUM && $to == GRADE_AGGREGATE_SUM && $this->weightoverride) {
1617
            // Do nothing. We are switching from SUM to SUM and the weight is overriden,
1618
            // a teacher would not expect any change in this situation.
1619
 
1620
        } else if ($from == GRADE_AGGREGATE_WEIGHTED_MEAN && $to == GRADE_AGGREGATE_WEIGHTED_MEAN) {
1621
            // Do nothing. The weights can be kept in this case.
1622
 
1623
        } else if (in_array($from, array(GRADE_AGGREGATE_SUM,  GRADE_AGGREGATE_EXTRACREDIT_MEAN, GRADE_AGGREGATE_WEIGHTED_MEAN2))
1624
                && in_array($to, array(GRADE_AGGREGATE_SUM,  GRADE_AGGREGATE_EXTRACREDIT_MEAN, GRADE_AGGREGATE_WEIGHTED_MEAN2))) {
1625
 
1626
            // Reset all but the the extra credit field.
1627
            $this->aggregationcoef2 = $defaults['aggregationcoef2'];
1628
            $this->weightoverride = $defaults['weightoverride'];
1629
 
1630
            if ($to != GRADE_AGGREGATE_EXTRACREDIT_MEAN) {
1631
                // Normalise extra credit, except for 'Mean with extra credit' which supports higher values than 1.
1632
                $this->aggregationcoef = min(1, $this->aggregationcoef);
1633
            }
1634
        } else {
1635
            // Reset all.
1636
            $this->aggregationcoef = $defaults['aggregationcoef'];
1637
            $this->aggregationcoef2 = $defaults['aggregationcoef2'];
1638
            $this->weightoverride = $defaults['weightoverride'];
1639
        }
1640
 
1641
        $acoefdiff       = grade_floats_different($origaggregationcoef, $this->aggregationcoef);
1642
        $acoefdiff2      = grade_floats_different($origaggregationcoef2, $this->aggregationcoef2);
1643
        $weightoverride  = grade_floats_different($origweighoverride, $this->weightoverride);
1644
 
1645
        return $acoefdiff || $acoefdiff2 || $weightoverride;
1646
    }
1647
 
1648
    /**
1649
     * Makes sure value is a valid grade value.
1650
     *
1651
     * @param float $gradevalue
1652
     * @return mixed float or int fixed grade value
1653
     */
1654
    public function bounded_grade($gradevalue) {
1655
        global $CFG;
1656
 
1657
        if (is_null($gradevalue)) {
1658
            return null;
1659
        }
1660
 
1661
        if ($this->gradetype == GRADE_TYPE_SCALE) {
1662
            // no >100% grades hack for scale grades!
1663
            // 1.5 is rounded to 2 ;-)
1664
            return (int)bounded_number($this->grademin, round($gradevalue+0.00001), $this->grademax);
1665
        }
1666
 
1667
        $grademax = $this->grademax;
1668
 
1669
        // NOTE: if you change this value you must manually reset the needsupdate flag in all grade items
1670
        $maxcoef = isset($CFG->gradeoverhundredprocentmax) ? $CFG->gradeoverhundredprocentmax : 10; // 1000% max by default
1671
 
1672
        if (!empty($CFG->unlimitedgrades)) {
1673
            // NOTE: if you change this value you must manually reset the needsupdate flag in all grade items
1674
            $grademax = $grademax * $maxcoef;
1675
        } else if ($this->is_category_item() or $this->is_course_item()) {
1676
            $category = $this->load_item_category();
1677
            if ($category->aggregation >= 100) {
1678
                // grade >100% hack
1679
                $grademax = $grademax * $maxcoef;
1680
            }
1681
        }
1682
 
1683
        return (float)bounded_number($this->grademin, $gradevalue, $grademax);
1684
    }
1685
 
1686
    /**
1687
     * Finds out on which other items does this depend directly when doing calculation or category aggregation
1688
     *
1689
     * @param bool $reset_cache
1690
     * @return array of grade_item IDs this one depends on
1691
     */
1692
    public function depends_on($reset_cache=false) {
1693
        global $CFG, $DB;
1694
 
1695
        if ($reset_cache) {
1696
            $this->dependson_cache = null;
1697
        } else if (isset($this->dependson_cache)) {
1698
            return $this->dependson_cache;
1699
        }
1700
 
1701
        if ($this->is_locked() && !$this->is_category_item()) {
1702
            // locked items do not need to be regraded
1703
            $this->dependson_cache = array();
1704
            return $this->dependson_cache;
1705
        }
1706
 
1707
        if ($this->is_calculated()) {
1708
            if (preg_match_all('/##gi(\d+)##/', $this->calculation, $matches)) {
1709
                $this->dependson_cache = array_unique($matches[1]); // remove duplicates
1710
                return $this->dependson_cache;
1711
            } else {
1712
                $this->dependson_cache = array();
1713
                return $this->dependson_cache;
1714
            }
1715
 
1716
        } else if ($grade_category = $this->load_item_category()) {
1717
            $params = array();
1718
 
1719
            //only items with numeric or scale values can be aggregated
1720
            if ($this->gradetype != GRADE_TYPE_VALUE and $this->gradetype != GRADE_TYPE_SCALE) {
1721
                $this->dependson_cache = array();
1722
                return $this->dependson_cache;
1723
            }
1724
 
1725
            $grade_category->apply_forced_settings();
1726
 
1727
            if (empty($CFG->enableoutcomes) or $grade_category->aggregateoutcomes) {
1728
                $outcomes_sql = "";
1729
            } else {
1730
                $outcomes_sql = "AND gi.outcomeid IS NULL";
1731
            }
1732
 
1733
            if (empty($CFG->grade_includescalesinaggregation)) {
1734
                $gtypes = "gi.gradetype = ?";
1735
                $params[] = GRADE_TYPE_VALUE;
1736
            } else {
1737
                $gtypes = "(gi.gradetype = ? OR gi.gradetype = ?)";
1738
                $params[] = GRADE_TYPE_VALUE;
1739
                $params[] = GRADE_TYPE_SCALE;
1740
            }
1741
 
1742
            $params[] = $grade_category->id;
1743
            $params[] = $this->courseid;
1744
            $params[] = $grade_category->id;
1745
            $params[] = $this->courseid;
1746
            if (empty($CFG->grade_includescalesinaggregation)) {
1747
                $params[] = GRADE_TYPE_VALUE;
1748
            } else {
1749
                $params[] = GRADE_TYPE_VALUE;
1750
                $params[] = GRADE_TYPE_SCALE;
1751
            }
1752
            $sql = "SELECT gi.id
1753
                      FROM {grade_items} gi
1754
                     WHERE $gtypes
1755
                           AND gi.categoryid = ?
1756
                           AND gi.courseid = ?
1757
                           $outcomes_sql
1758
                    UNION
1759
 
1760
                    SELECT gi.id
1761
                      FROM {grade_items} gi, {grade_categories} gc
1762
                     WHERE (gi.itemtype = 'category' OR gi.itemtype = 'course') AND gi.iteminstance=gc.id
1763
                           AND gc.parent = ?
1764
                           AND gi.courseid = ?
1765
                           AND $gtypes
1766
                           $outcomes_sql";
1767
 
1768
            if ($children = $DB->get_records_sql($sql, $params)) {
1769
                $this->dependson_cache = array_keys($children);
1770
                return $this->dependson_cache;
1771
            } else {
1772
                $this->dependson_cache = array();
1773
                return $this->dependson_cache;
1774
            }
1775
 
1776
        } else {
1777
            $this->dependson_cache = array();
1778
            return $this->dependson_cache;
1779
        }
1780
    }
1781
 
1782
    /**
1783
     * Refetch grades from modules, plugins.
1784
     *
1785
     * @param int $userid optional, limit the refetch to a single user
1786
     * @return bool Returns true on success or if there is nothing to do
1787
     */
1788
    public function refresh_grades($userid=0) {
1789
        global $DB;
1790
        if ($this->itemtype == 'mod') {
1791
            if ($this->is_outcome_item()) {
1792
                //nothing to do
1793
                return true;
1794
            }
1795
 
1796
            if (!$activity = $DB->get_record($this->itemmodule, array('id' => $this->iteminstance))) {
1797
                debugging("Can not find $this->itemmodule activity with id $this->iteminstance");
1798
                return false;
1799
            }
1800
 
1801
            if (!$cm = get_coursemodule_from_instance($this->itemmodule, $activity->id, $this->courseid)) {
1802
                debugging('Can not find course module');
1803
                return false;
1804
            }
1805
 
1806
            $activity->modname    = $this->itemmodule;
1807
            $activity->cmidnumber = $cm->idnumber;
1808
 
1809
            return grade_update_mod_grades($activity, $userid);
1810
        }
1811
 
1812
        return true;
1813
    }
1814
 
1815
    /**
1816
     * Updates final grade value for given user, this is a only way to update final
1817
     * grades from gradebook and import because it logs the change in history table
1818
     * and deals with overridden flag. This flag is set to prevent later overriding
1819
     * from raw grades submitted from modules.
1820
     *
1821
     * @param int $userid The graded user
1822
     * @param float|false $finalgrade The float value of final grade, false means do not change
1823
     * @param string $source The modification source
1824
     * @param string $feedback Optional teacher feedback
1825
     * @param int $feedbackformat A format like FORMAT_PLAIN or FORMAT_HTML
1826
     * @param int $usermodified The ID of the user making the modification
1827
     * @param int $timemodified Optional parameter to set the time modified, if not present current time.
1828
     * @param bool $isbulkupdate If bulk grade update is happening.
1829
     * @return bool success
1830
     */
1831
    public function update_final_grade($userid, $finalgrade = false, $source = null, $feedback = false,
1832
            $feedbackformat = FORMAT_MOODLE, $usermodified = null, $timemodified = null, $isbulkupdate = false) {
1833
        global $USER, $CFG;
1834
 
1835
        $result = true;
1836
 
1837
        // no grading used or locked
1838
        if ($this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) {
1839
            return false;
1840
        }
1841
 
1842
        $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid));
1843
        $grade->grade_item =& $this; // prevent db fetching of this grade_item
1844
 
1845
        if (empty($usermodified)) {
1846
            $grade->usermodified = $USER->id;
1847
        } else {
1848
            $grade->usermodified = $usermodified;
1849
        }
1850
 
1851
        if ($grade->is_locked()) {
1852
            // do not update locked grades at all
1853
            return false;
1854
        }
1855
 
1856
        $locktime = $grade->get_locktime();
1857
        if ($locktime and $locktime < time()) {
1858
            // do not update grades that should be already locked, force regrade instead
1859
            $this->force_regrading();
1860
            return false;
1861
        }
1862
 
1863
        $oldgrade = new stdClass();
1864
        $oldgrade->finalgrade     = $grade->finalgrade;
1865
        $oldgrade->overridden     = $grade->overridden;
1866
        $oldgrade->feedback       = $grade->feedback;
1867
        $oldgrade->feedbackformat = $grade->feedbackformat;
1868
        $oldgrade->rawgrademin    = $grade->rawgrademin;
1869
        $oldgrade->rawgrademax    = $grade->rawgrademax;
1870
 
1871
        // MDL-31713 rawgramemin and max must be up to date so conditional access %'s works properly.
1872
        $grade->rawgrademin = $this->grademin;
1873
        $grade->rawgrademax = $this->grademax;
1874
        $grade->rawscaleid  = $this->scaleid;
1875
 
1876
        // changed grade?
1877
        if ($finalgrade !== false) {
1878
            if ($this->is_overridable_item() && $this->markasoverriddenwhengraded) {
1879
                $grade->overridden = time();
1880
            }
1881
 
1882
            $grade->finalgrade = $this->bounded_grade($finalgrade);
1883
        }
1884
 
1885
        // do we have comment from teacher?
1886
        if ($feedback !== false) {
1887
            if ($this->is_overridable_item_feedback()) {
1888
                // external items (modules, plugins) may have own feedback
1889
                $grade->overridden = time();
1890
            }
1891
 
1892
            $grade->feedback       = $feedback;
1893
            $grade->feedbackformat = $feedbackformat;
1894
        }
1895
 
1896
        $gradechanged = false;
1897
        if (empty($grade->id)) {
1898
            $grade->timecreated = null;   // Hack alert - date submitted - no submission yet.
1899
            $grade->timemodified = $timemodified ?? time(); // Hack alert - date graded.
1900
            $result = (bool)$grade->insert($source, $isbulkupdate);
1901
 
1902
            // If the grade insert was successful and the final grade was not null then trigger a user_graded event.
1903
            if ($result && !is_null($grade->finalgrade)) {
1904
                \core\event\user_graded::create_from_grade($grade)->trigger();
1905
            }
1906
            $gradechanged = true;
1907
        } else {
1908
            // Existing grade_grades.
1909
 
1910
            if (grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)
1911
                    or grade_floats_different($grade->rawgrademin, $oldgrade->rawgrademin)
1912
                    or grade_floats_different($grade->rawgrademax, $oldgrade->rawgrademax)
1913
                    or ($oldgrade->overridden == 0 and $grade->overridden > 0)) {
1914
                $gradechanged = true;
1915
            }
1916
 
1917
            if ($grade->feedback === $oldgrade->feedback and $grade->feedbackformat == $oldgrade->feedbackformat and
1918
                    $gradechanged === false) {
1919
                // No grade nor feedback changed.
1920
                return $result;
1921
            }
1922
 
1923
            $grade->timemodified = $timemodified ?? time(); // Hack alert - date graded.
1924
            $result = $grade->update($source, $isbulkupdate);
1925
 
1926
            // If the grade update was successful and the actual grade has changed then trigger a user_graded event.
1927
            if ($result && grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)) {
1928
                \core\event\user_graded::create_from_grade($grade)->trigger();
1929
            }
1930
        }
1931
 
1932
        if (!$result) {
1933
            // Something went wrong - better force final grade recalculation.
1934
            $this->force_regrading();
1935
            return $result;
1936
        }
1937
 
1938
        // If we are not updating grades we don't need to recalculate the whole course.
1939
        if (!$gradechanged) {
1940
            return $result;
1941
        }
1942
 
1943
        if ($this->is_course_item() and !$this->needsupdate) {
1944
            if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
1945
                $this->force_regrading();
1946
            }
1947
 
1948
        } else if (!$this->needsupdate) {
1949
 
1950
            $course_item = grade_item::fetch_course_item($this->courseid);
1951
            if (!$course_item->needsupdate) {
1952
                if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
1953
                    $this->force_regrading();
1954
                }
1955
            } else {
1956
                $this->force_regrading();
1957
            }
1958
        }
1959
 
1960
        return $result;
1961
    }
1962
 
1963
 
1964
    /**
1965
     * Updates raw grade value for given user, this is a only way to update raw
1966
     * grades from external source (modules, etc.),
1967
     * because it logs the change in history table and deals with final grade recalculation.
1968
     *
1969
     * @param int $userid the graded user
1970
     * @param mixed $rawgrade float value of raw grade - false means do not change
1971
     * @param string $source modification source
1972
     * @param string $feedback optional teacher feedback
1973
     * @param int $feedbackformat A format like FORMAT_PLAIN or FORMAT_HTML
1974
     * @param int $usermodified the ID of the user who did the grading
1975
     * @param int $dategraded A timestamp of when the student's work was graded
1976
     * @param int $datesubmitted A timestamp of when the student's work was submitted
1977
     * @param grade_grade $grade A grade object, useful for bulk upgrades
1978
     * @param array $feedbackfiles An array identifying the location of files we want to copy to the gradebook feedback area.
1979
     *        Example -
1980
     *        [
1981
     *            'contextid' => 1,
1982
     *            'component' => 'mod_xyz',
1983
     *            'filearea' => 'mod_xyz_feedback',
1984
     *            'itemid' => 2
1985
     *        ];
1986
     * @param bool $isbulkupdate If bulk grade update is happening.
1987
     * @return bool success
1988
     */
1989
    public function update_raw_grade($userid, $rawgrade = false, $source = null, $feedback = false,
1990
            $feedbackformat = FORMAT_MOODLE, $usermodified = null, $dategraded = null, $datesubmitted=null,
1991
            $grade = null, array $feedbackfiles = [], $isbulkupdate = false) {
1992
        global $USER;
1993
 
1994
        $result = true;
1995
 
1996
        // calculated grades can not be updated; course and category can not be updated  because they are aggregated
1997
        if (!$this->is_raw_used() or $this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) {
1998
            return false;
1999
        }
2000
 
2001
        if (is_null($grade)) {
2002
            //fetch from db
2003
            $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid));
2004
        }
2005
        $grade->grade_item =& $this; // prevent db fetching of this grade_item
2006
 
2007
        if (empty($usermodified)) {
2008
            $grade->usermodified = $USER->id;
2009
        } else {
2010
            $grade->usermodified = $usermodified;
2011
        }
2012
 
2013
        if ($grade->is_locked()) {
2014
            // do not update locked grades at all
2015
            return false;
2016
        }
2017
 
2018
        $locktime = $grade->get_locktime();
2019
        if ($locktime and $locktime < time()) {
2020
            // do not update grades that should be already locked and force regrade
2021
            $this->force_regrading();
2022
            return false;
2023
        }
2024
 
2025
        $oldgrade = new stdClass();
2026
        $oldgrade->finalgrade     = $grade->finalgrade;
2027
        $oldgrade->rawgrade       = $grade->rawgrade;
2028
        $oldgrade->rawgrademin    = $grade->rawgrademin;
2029
        $oldgrade->rawgrademax    = $grade->rawgrademax;
2030
        $oldgrade->rawscaleid     = $grade->rawscaleid;
2031
        $oldgrade->feedback       = $grade->feedback;
2032
        $oldgrade->feedbackformat = $grade->feedbackformat;
2033
 
2034
        // use new min and max
2035
        $grade->rawgrade    = $grade->rawgrade;
2036
        $grade->rawgrademin = $this->grademin;
2037
        $grade->rawgrademax = $this->grademax;
2038
        $grade->rawscaleid  = $this->scaleid;
2039
 
2040
        // change raw grade?
2041
        if ($rawgrade !== false) {
2042
            $grade->rawgrade = $rawgrade;
2043
        }
2044
 
2045
        // empty feedback means no feedback at all
2046
        if ($feedback === '') {
2047
            $feedback = null;
2048
        }
2049
 
2050
        // do we have comment from teacher?
2051
        if ($feedback !== false and !$grade->is_overridden()) {
2052
            $grade->feedback       = $feedback;
2053
            $grade->feedbackformat = $feedbackformat;
2054
            $grade->feedbackfiles  = $feedbackfiles;
2055
        }
2056
 
2057
        // update final grade if possible
2058
        if (!$grade->is_locked() and !$grade->is_overridden()) {
2059
            $grade->finalgrade = $this->adjust_raw_grade($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
2060
        }
2061
 
2062
        // TODO: hack alert - create new fields for these in 2.0
2063
        $oldgrade->timecreated  = $grade->timecreated;
2064
        $oldgrade->timemodified = $grade->timemodified;
2065
 
2066
        $grade->timecreated = $datesubmitted;
2067
 
2068
        if ($grade->is_overridden()) {
2069
            // keep original graded date - update_final_grade() sets this for overridden grades
2070
 
2071
        } else if (is_null($grade->rawgrade) and is_null($grade->feedback)) {
2072
            // no grade and feedback means no grading yet
2073
            $grade->timemodified = null;
2074
 
2075
        } else if (!empty($dategraded)) {
2076
            // fine - module sends info when graded (yay!)
2077
            $grade->timemodified = $dategraded;
2078
 
2079
        } else if (grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)
2080
                   or $grade->feedback !== $oldgrade->feedback) {
2081
            // guess - if either grade or feedback changed set new graded date
2082
            $grade->timemodified = time();
2083
 
2084
        } else {
2085
            //keep original graded date
2086
        }
2087
        // end of hack alert
2088
 
2089
        $gradechanged = false;
2090
        if (empty($grade->id)) {
2091
            $result = (bool)$grade->insert($source, $isbulkupdate);
2092
 
2093
            // If the grade insert was successful and the final grade was not null then trigger a user_graded event.
2094
            if ($result && !is_null($grade->finalgrade)) {
2095
                \core\event\user_graded::create_from_grade($grade)->trigger();
2096
            }
2097
            $gradechanged = true;
2098
        } else {
2099
            // Existing grade_grades.
2100
 
2101
            if (grade_floats_different($grade->finalgrade,  $oldgrade->finalgrade)
2102
                    or grade_floats_different($grade->rawgrade,    $oldgrade->rawgrade)
2103
                    or grade_floats_different($grade->rawgrademin, $oldgrade->rawgrademin)
2104
                    or grade_floats_different($grade->rawgrademax, $oldgrade->rawgrademax)
2105
                    or $grade->rawscaleid != $oldgrade->rawscaleid) {
2106
                $gradechanged = true;
2107
            }
2108
 
2109
            // The timecreated and timemodified checking is part of the hack above.
2110
            if ($gradechanged === false and
2111
                    $grade->feedback === $oldgrade->feedback and
2112
                    $grade->feedbackformat == $oldgrade->feedbackformat and
2113
                    $grade->timecreated == $oldgrade->timecreated and
2114
                    $grade->timemodified == $oldgrade->timemodified) {
2115
                // No changes.
2116
                return $result;
2117
            }
2118
            $result = $grade->update($source, $isbulkupdate);
2119
 
2120
            // If the grade update was successful and the actual grade has changed then trigger a user_graded event.
2121
            if ($result && grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)) {
2122
                \core\event\user_graded::create_from_grade($grade)->trigger();
2123
            }
2124
        }
2125
 
2126
        if (!$result) {
2127
            // Something went wrong - better force final grade recalculation.
2128
            $this->force_regrading();
2129
            return $result;
2130
        }
2131
 
2132
        // If we are not updating grades we don't need to recalculate the whole course.
2133
        if (!$gradechanged) {
2134
            return $result;
2135
        }
2136
 
2137
        if (!$this->needsupdate) {
2138
            $course_item = grade_item::fetch_course_item($this->courseid);
2139
            if (!$course_item->needsupdate) {
2140
                if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
2141
                    $this->force_regrading();
2142
                }
2143
            }
2144
        }
2145
 
2146
        return $result;
2147
    }
2148
 
2149
    /**
2150
     * Calculates final grade values using the formula in the calculation property.
2151
     * The parameters are taken from final grades of grade items in current course only.
2152
     *
2153
     * @param int $userid Supply a user ID to limit the calculations to the grades of a single user
2154
     * @return bool false if error
2155
     */
2156
    public function compute($userid=null) {
2157
        global $CFG, $DB;
2158
 
2159
        if (!$this->is_calculated()) {
2160
            return false;
2161
        }
2162
 
2163
        require_once($CFG->libdir.'/mathslib.php');
2164
 
2165
        if ($this->is_locked()) {
2166
            return true; // no need to recalculate locked items
2167
        }
2168
 
2169
        // Precreate grades - we need them to exist
2170
        if ($userid) {
2171
            $missing = array();
2172
            if (!$DB->record_exists('grade_grades', array('itemid'=>$this->id, 'userid'=>$userid))) {
2173
                $m = new stdClass();
2174
                $m->userid = $userid;
2175
                $missing[] = $m;
2176
            }
2177
        } else {
2178
            // Find any users who have grades for some but not all grade items in this course
2179
            $params = array('gicourseid' => $this->courseid, 'ggitemid' => $this->id);
2180
            $sql = "SELECT gg.userid
2181
                      FROM {grade_grades} gg
2182
                           JOIN {grade_items} gi
2183
                           ON (gi.id = gg.itemid AND gi.courseid = :gicourseid)
2184
                     GROUP BY gg.userid
2185
                     HAVING SUM(CASE WHEN gg.itemid = :ggitemid THEN 1 ELSE 0 END) = 0";
2186
            $missing = $DB->get_records_sql($sql, $params);
2187
        }
2188
 
2189
        if ($missing) {
2190
            foreach ($missing as $m) {
2191
                $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$m->userid), false);
2192
                $grade->grade_item =& $this;
2193
                $grade->insert('system');
2194
            }
2195
        }
2196
 
2197
        // get used items
2198
        $useditems = $this->depends_on();
2199
 
2200
        // prepare formula and init maths library
2201
        $formula = preg_replace('/##(gi\d+)##/', '\1', $this->calculation);
2202
        if (strpos($formula, '[[') !== false) {
2203
            // missing item
2204
            return false;
2205
        }
2206
        $this->formula = new calc_formula($formula);
2207
 
2208
        // where to look for final grades?
2209
        // this itemid is added so that we use only one query for source and final grades
2210
        $gis = array_merge($useditems, array($this->id));
2211
        list($usql, $params) = $DB->get_in_or_equal($gis);
2212
 
2213
        if ($userid) {
2214
            $usersql = "AND g.userid=?";
2215
            $params[] = $userid;
2216
        } else {
2217
            $usersql = "";
2218
        }
2219
 
2220
        $grade_inst = new grade_grade();
2221
        $fields = 'g.'.implode(',g.', $grade_inst->required_fields);
2222
 
2223
        $params[] = $this->courseid;
2224
        $sql = "SELECT $fields
2225
                  FROM {grade_grades} g, {grade_items} gi
2226
                 WHERE gi.id = g.itemid AND gi.id $usql $usersql AND gi.courseid=?
2227
                 ORDER BY g.userid";
2228
 
2229
        $return = true;
2230
 
2231
        // group the grades by userid and use formula on the group
2232
        $rs = $DB->get_recordset_sql($sql, $params);
2233
        if ($rs->valid()) {
2234
            $prevuser = 0;
2235
            $grade_records   = array();
2236
            $oldgrade    = null;
2237
            foreach ($rs as $used) {
2238
                if ($used->userid != $prevuser) {
2239
                    if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {
2240
                        $return = false;
2241
                    }
2242
                    $prevuser = $used->userid;
2243
                    $grade_records   = array();
2244
                    $oldgrade    = null;
2245
                }
2246
                if ($used->itemid == $this->id) {
2247
                    $oldgrade = $used;
2248
                }
2249
                $grade_records['gi'.$used->itemid] = $used->finalgrade;
2250
            }
2251
            if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {
2252
                $return = false;
2253
            }
2254
        }
2255
        $rs->close();
2256
 
2257
        return $return;
2258
    }
2259
 
2260
    /**
2261
     * Internal function that does the final grade calculation
2262
     *
2263
     * @param int $userid The user ID
2264
     * @param array $params An array of grade items of the form {'gi'.$itemid]} => $finalgrade
2265
     * @param array $useditems An array of grade item IDs that this grade item depends on plus its own ID
2266
     * @param grade_grade $oldgrade A grade_grade instance containing the old values from the database
2267
     * @return bool False if an error occurred
2268
     */
2269
    public function use_formula($userid, $params, $useditems, $oldgrade) {
2270
        if (empty($userid)) {
2271
            return true;
2272
        }
2273
 
2274
        // add missing final grade values
2275
        // not graded (null) is counted as 0 - the spreadsheet way
2276
        $allinputsnull = true;
2277
        foreach($useditems as $gi) {
2278
            if (!array_key_exists('gi'.$gi, $params) || is_null($params['gi'.$gi])) {
2279
                $params['gi'.$gi] = 0;
2280
            } else {
2281
                $params['gi'.$gi] = (float)$params['gi'.$gi];
2282
                if ($gi != $this->id) {
2283
                    $allinputsnull = false;
2284
                }
2285
            }
2286
        }
2287
 
2288
        // can not use own final grade during calculation
2289
        unset($params['gi'.$this->id]);
2290
 
2291
        // Check to see if the gradebook is frozen. This allows grades to not be altered at all until a user verifies that they
2292
        // wish to update the grades.
2293
        $gradebookcalculationsfreeze = get_config('core', 'gradebook_calculations_freeze_' . $this->courseid);
2294
 
2295
        $rawminandmaxchanged = false;
2296
        // insert final grade - will be needed later anyway
2297
        if ($oldgrade) {
2298
            // Only run through this code if the gradebook isn't frozen.
2299
            if ($gradebookcalculationsfreeze && (int)$gradebookcalculationsfreeze <= 20150627) {
2300
                // Do nothing.
2301
            } else {
2302
                // The grade_grade for a calculated item should have the raw grade maximum and minimum set to the
2303
                // grade_item grade maximum and minimum respectively.
2304
                if ($oldgrade->rawgrademax != $this->grademax || $oldgrade->rawgrademin != $this->grademin) {
2305
                    $rawminandmaxchanged = true;
2306
                    $oldgrade->rawgrademax = $this->grademax;
2307
                    $oldgrade->rawgrademin = $this->grademin;
2308
                }
2309
            }
2310
            $oldfinalgrade = $oldgrade->finalgrade;
2311
            $grade = new grade_grade($oldgrade, false); // fetching from db is not needed
2312
            $grade->grade_item =& $this;
2313
 
2314
        } else {
2315
            $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid), false);
2316
            $grade->grade_item =& $this;
2317
            $rawminandmaxchanged = false;
2318
            if ($gradebookcalculationsfreeze && (int)$gradebookcalculationsfreeze <= 20150627) {
2319
                // Do nothing.
2320
            } else {
2321
                // The grade_grade for a calculated item should have the raw grade maximum and minimum set to the
2322
                // grade_item grade maximum and minimum respectively.
2323
                $rawminandmaxchanged = true;
2324
                $grade->rawgrademax = $this->grademax;
2325
                $grade->rawgrademin = $this->grademin;
2326
            }
2327
            $grade->insert('system');
2328
            $oldfinalgrade = null;
2329
        }
2330
 
2331
        // no need to recalculate locked or overridden grades
2332
        if ($grade->is_locked() or $grade->is_overridden()) {
2333
            return true;
2334
        }
2335
 
2336
        if ($allinputsnull) {
2337
            $grade->finalgrade = null;
2338
            $result = true;
2339
 
2340
        } else {
2341
 
2342
            // do the calculation
2343
            $this->formula->set_params($params);
2344
            $result = $this->formula->evaluate();
2345
 
2346
            if ($result === false) {
2347
                $grade->finalgrade = null;
2348
 
2349
            } else {
2350
                // normalize
2351
                $grade->finalgrade = $this->bounded_grade($result);
2352
            }
2353
        }
2354
 
2355
        // Only run through this code if the gradebook isn't frozen.
2356
        if ($gradebookcalculationsfreeze && (int)$gradebookcalculationsfreeze <= 20150627) {
2357
            // Update in db if changed.
2358
            if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) {
2359
                $grade->timemodified = time();
2360
                $success = $grade->update('compute');
2361
 
2362
                // If successful trigger a user_graded event.
2363
                if ($success) {
2364
                    \core\event\user_graded::create_from_grade($grade)->trigger();
2365
                }
2366
            }
2367
        } else {
2368
            // Update in db if changed.
2369
            if (grade_floats_different($grade->finalgrade, $oldfinalgrade) || $rawminandmaxchanged) {
2370
                $grade->timemodified = time();
2371
                $success = $grade->update('compute');
2372
 
2373
                // If successful trigger a user_graded event.
2374
                if ($success) {
2375
                    \core\event\user_graded::create_from_grade($grade)->trigger();
2376
                }
2377
            }
2378
        }
2379
 
2380
        if ($result !== false) {
2381
            //lock grade if needed
2382
        }
2383
 
2384
        if ($result === false) {
2385
            return false;
2386
        } else {
2387
            return true;
2388
        }
2389
 
2390
    }
2391
 
2392
    /**
2393
     * Validate the formula.
2394
     *
2395
     * @param string $formulastr
2396
     * @return bool true if calculation possible, false otherwise
2397
     */
2398
    public function validate_formula($formulastr) {
2399
        global $CFG, $DB;
2400
        require_once($CFG->libdir.'/mathslib.php');
2401
 
2402
        $formulastr = grade_item::normalize_formula($formulastr, $this->courseid);
2403
 
2404
        if (empty($formulastr)) {
2405
            return true;
2406
        }
2407
 
2408
        if (strpos($formulastr, '=') !== 0) {
2409
            return get_string('errorcalculationnoequal', 'grades');
2410
        }
2411
 
2412
        // get used items
2413
        if (preg_match_all('/##gi(\d+)##/', $formulastr, $matches)) {
2414
            $useditems = array_unique($matches[1]); // remove duplicates
2415
        } else {
2416
            $useditems = array();
2417
        }
2418
 
2419
        // MDL-11902
2420
        // unset the value if formula is trying to reference to itself
2421
        // but array keys does not match itemid
2422
        if (!empty($this->id)) {
2423
            $useditems = array_diff($useditems, array($this->id));
2424
            //unset($useditems[$this->id]);
2425
        }
2426
 
2427
        // prepare formula and init maths library
2428
        $formula = preg_replace('/##(gi\d+)##/', '\1', $formulastr);
2429
        $formula = new calc_formula($formula);
2430
 
2431
 
2432
        if (empty($useditems)) {
2433
            $grade_items = array();
2434
 
2435
        } else {
2436
            list($usql, $params) = $DB->get_in_or_equal($useditems);
2437
            $params[] = $this->courseid;
2438
            $sql = "SELECT gi.*
2439
                      FROM {grade_items} gi
2440
                     WHERE gi.id $usql and gi.courseid=?"; // from the same course only!
2441
 
2442
            if (!$grade_items = $DB->get_records_sql($sql, $params)) {
2443
                $grade_items = array();
2444
            }
2445
        }
2446
 
2447
        $params = array();
2448
        foreach ($useditems as $itemid) {
2449
            // make sure all grade items exist in this course
2450
            if (!array_key_exists($itemid, $grade_items)) {
2451
                return false;
2452
            }
2453
            // use max grade when testing formula, this should be ok in 99.9%
2454
            // division by 0 is one of possible problems
2455
            $params['gi'.$grade_items[$itemid]->id] = $grade_items[$itemid]->grademax;
2456
        }
2457
 
2458
        // do the calculation
2459
        $formula->set_params($params);
2460
        $result = $formula->evaluate();
2461
 
2462
        // false as result indicates some problem
2463
        if ($result === false) {
2464
            // TODO: add more error hints
2465
            return get_string('errorcalculationunknown', 'grades');
2466
        } else {
2467
            return true;
2468
        }
2469
    }
2470
 
2471
    /**
2472
     * Returns the value of the display type
2473
     *
2474
     * It can be set at 3 levels: grade_item, course setting and site. The lowest level overrides the higher ones.
2475
     *
2476
     * @return int Display type
2477
     */
2478
    public function get_displaytype() {
2479
        global $CFG;
2480
 
2481
        if ($this->display == GRADE_DISPLAY_TYPE_DEFAULT) {
2482
            return grade_get_setting($this->courseid, 'displaytype', $CFG->grade_displaytype);
2483
 
2484
        } else {
2485
            return $this->display;
2486
        }
2487
    }
2488
 
2489
    /**
2490
     * Returns the value of the decimals field
2491
     *
2492
     * It can be set at 3 levels: grade_item, course setting and site. The lowest level overrides the higher ones.
2493
     *
2494
     * @return int Decimals (0 - 5)
2495
     */
2496
    public function get_decimals() {
2497
        global $CFG;
2498
 
2499
        if (is_null($this->decimals)) {
2500
            return grade_get_setting($this->courseid, 'decimalpoints', $CFG->grade_decimalpoints);
2501
 
2502
        } else {
2503
            return $this->decimals;
2504
        }
2505
    }
2506
 
2507
    /**
2508
     * Returns a string representing the range of grademin - grademax for this grade item.
2509
     *
2510
     * @param int $rangesdisplaytype
2511
     * @param int $rangesdecimalpoints
2512
     * @return string
2513
     */
2514
    function get_formatted_range($rangesdisplaytype=null, $rangesdecimalpoints=null) {
2515
 
2516
        global $USER;
2517
 
2518
        // Determine which display type to use for this average
2519
        if (isset($USER->editing) && $USER->editing) {
2520
            $displaytype = GRADE_DISPLAY_TYPE_REAL;
2521
 
2522
        } else if ($rangesdisplaytype == GRADE_REPORT_PREFERENCE_INHERIT) { // no ==0 here, please resave report and user prefs
2523
            $displaytype = $this->get_displaytype();
2524
 
2525
        } else {
2526
            $displaytype = $rangesdisplaytype;
2527
        }
2528
 
2529
        // Override grade_item setting if a display preference (not default) was set for the averages
2530
        if ($rangesdecimalpoints == GRADE_REPORT_PREFERENCE_INHERIT) {
2531
            $decimalpoints = $this->get_decimals();
2532
 
2533
        } else {
2534
            $decimalpoints = $rangesdecimalpoints;
2535
        }
2536
 
2537
        if ($displaytype == GRADE_DISPLAY_TYPE_PERCENTAGE) {
2538
            $grademin = "0 %";
2539
            $grademax = "100 %";
2540
 
2541
        } else {
2542
            $grademin = grade_format_gradevalue($this->grademin, $this, true, $displaytype, $decimalpoints);
2543
            $grademax = grade_format_gradevalue($this->grademax, $this, true, $displaytype, $decimalpoints);
2544
        }
2545
 
2546
        return $grademin.'&ndash;'. $grademax;
2547
    }
2548
 
2549
    /**
2550
     * Queries parent categories recursively to find the aggregationcoef type that applies to this grade item.
2551
     *
2552
     * @return string|false Returns the coefficient string of false is no coefficient is being used
2553
     */
2554
    public function get_coefstring() {
2555
        $parent_category = $this->load_parent_category();
2556
        if ($this->is_category_item()) {
2557
            $parent_category = $parent_category->load_parent_category();
2558
        }
2559
 
2560
        if ($parent_category->is_aggregationcoef_used()) {
2561
            return $parent_category->get_coefstring();
2562
        } else {
2563
            return false;
2564
        }
2565
    }
2566
 
2567
    /**
2568
     * Returns whether the grade item can control the visibility of the grades
2569
     *
2570
     * @return bool
2571
     */
2572
    public function can_control_visibility() {
2573
        if (core_component::get_plugin_directory($this->itemtype, $this->itemmodule)) {
2574
            return !plugin_supports($this->itemtype, $this->itemmodule, FEATURE_CONTROLS_GRADE_VISIBILITY, false);
2575
        }
2576
        return parent::can_control_visibility();
2577
    }
2578
 
2579
    /**
2580
     * Used to notify the completion system (if necessary) that a user's grade
2581
     * has changed, and clear up a possible score cache.
2582
     *
2583
     * @param bool $deleted True if grade was actually deleted
2584
     */
2585
    protected function notify_changed($deleted) {
2586
        global $CFG;
2587
 
2588
        // Condition code may cache the grades for conditional availability of
2589
        // modules or sections. (This code should use a hook for communication
2590
        // with plugin, but hooks are not implemented at time of writing.)
2591
        if (!empty($CFG->enableavailability) && class_exists('\availability_grade\callbacks')) {
2592
            \availability_grade\callbacks::grade_item_changed($this->courseid);
2593
        }
2594
    }
2595
 
2596
    /**
2597
     * Helper function to get the accurate context for this grade column.
2598
     *
2599
     * @return context
2600
     */
2601
    public function get_context() {
2602
        if ($this->itemtype == 'mod') {
2603
            $modinfo = get_fast_modinfo($this->courseid);
2604
            // Sometimes the course module cache is out of date and needs to be rebuilt.
2605
            if (!isset($modinfo->instances[$this->itemmodule][$this->iteminstance])) {
2606
                rebuild_course_cache($this->courseid, true);
2607
                $modinfo = get_fast_modinfo($this->courseid);
2608
            }
2609
            // Even with a rebuilt cache the module does not exist. This means the
2610
            // database is in an invalid state - we will log an error and return
2611
            // the course context but the calling code should be updated.
2612
            if (!isset($modinfo->instances[$this->itemmodule][$this->iteminstance])) {
2613
                mtrace(get_string('moduleinstancedoesnotexist', 'error'));
2614
                $context = \context_course::instance($this->courseid);
2615
            } else {
2616
                $cm = $modinfo->instances[$this->itemmodule][$this->iteminstance];
2617
                $context = \context_module::instance($cm->id);
2618
            }
2619
        } else {
2620
            $context = \context_course::instance($this->courseid);
2621
        }
2622
        return $context;
2623
    }
2624
}