Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | 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
     */
1441 ariadna 781
    public function has_hidden_grades($groupsql="", ?array $params=null, $groupwheresql="") {
1 efrain 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
 
1441 ariadna 1225
 
1 efrain 1226
    /**
1441 ariadna 1227
     * Returns whether the item is gradable or not. It's considered gradable when there is at least one gradeitem
1228
     * set as GRADE_TYPE_VALUE or GRADE_TYPE_SCALE.
1229
     *
1230
     * @return bool
1231
     */
1232
    public function is_gradable(): bool {
1233
        return $this->gradetype == GRADE_TYPE_VALUE || $this->gradetype == GRADE_TYPE_SCALE;
1234
    }
1235
 
1236
    /**
1 efrain 1237
     * Returns the grade item associated with the course
1238
     *
1239
     * @param int $courseid
1240
     * @return grade_item Course level grade item object
1241
     */
1242
    public static function fetch_course_item($courseid) {
1243
        if ($course_item = grade_item::fetch(array('courseid'=>$courseid, 'itemtype'=>'course'))) {
1244
            return $course_item;
1245
        }
1246
 
1247
        // first get category - it creates the associated grade item
1248
        $course_category = grade_category::fetch_course_category($courseid);
1249
        return $course_category->get_grade_item();
1250
    }
1251
 
1252
    /**
1253
     * Is grading object editable?
1254
     *
1255
     * @return bool
1256
     */
1257
    public function is_editable() {
1258
        return true;
1259
    }
1260
 
1261
    /**
1262
     * Checks if grade calculated. Returns this object's calculation.
1263
     *
1264
     * @return bool true if grade item calculated.
1265
     */
1266
    public function is_calculated() {
1267
        if (empty($this->calculation)) {
1268
            return false;
1269
        }
1270
 
1271
        /*
1272
         * The main reason why we use the ##gixxx## instead of [[idnumber]] is speed of depends_on(),
1273
         * we would have to fetch all course grade items to find out the ids.
1274
         * Also if user changes the idnumber the formula does not need to be updated.
1275
         */
1276
 
1277
        // first detect if we need to change calculation formula from [[idnumber]] to ##giXXX## (after backup, etc.)
1278
        if (!$this->calculation_normalized and strpos($this->calculation, '[[') !== false) {
1279
            $this->set_calculation($this->calculation);
1280
        }
1281
 
1282
        return !empty($this->calculation);
1283
    }
1284
 
1285
    /**
1286
     * Returns calculation string if grade calculated.
1287
     *
1288
     * @return string Returns the grade item's calculation if calculation is used, null if not
1289
     */
1290
    public function get_calculation() {
1291
        if ($this->is_calculated()) {
1292
            return grade_item::denormalize_formula($this->calculation, $this->courseid);
1293
 
1294
        } else {
1295
            return NULL;
1296
        }
1297
    }
1298
 
1299
    /**
1300
     * Sets this item's calculation (creates it) if not yet set, or
1301
     * updates it if already set (in the DB). If no calculation is given,
1302
     * the calculation is removed.
1303
     *
1304
     * @param string $formula string representation of formula used for calculation
1305
     * @return bool success
1306
     */
1307
    public function set_calculation($formula) {
1308
        $this->calculation = grade_item::normalize_formula($formula, $this->courseid);
1309
        $this->calculation_normalized = true;
1310
        return $this->update();
1311
    }
1312
 
1313
    /**
1314
     * Denormalizes the calculation formula to [idnumber] form
1315
     *
1316
     * @param string $formula A string representation of the formula
1317
     * @param int $courseid The course ID
1318
     * @return string The denormalized formula as a string
1319
     */
1320
    public static function denormalize_formula($formula, $courseid) {
1321
        if (empty($formula)) {
1322
            return '';
1323
        }
1324
 
1325
        // denormalize formula - convert ##giXX## to [[idnumber]]
1326
        if (preg_match_all('/##gi(\d+)##/', $formula, $matches)) {
1327
            foreach ($matches[1] as $id) {
1328
                if ($grade_item = grade_item::fetch(array('id'=>$id, 'courseid'=>$courseid))) {
1329
                    if (!empty($grade_item->idnumber)) {
1330
                        $formula = str_replace('##gi'.$grade_item->id.'##', '[['.$grade_item->idnumber.']]', $formula);
1331
                    }
1332
                }
1333
            }
1334
        }
1335
 
1336
        return $formula;
1337
 
1338
    }
1339
 
1340
    /**
1341
     * Normalizes the calculation formula to [#giXX#] form
1342
     *
1343
     * @param string $formula The formula
1344
     * @param int $courseid The course ID
1345
     * @return string The normalized formula as a string
1346
     */
1347
    public static function normalize_formula($formula, $courseid) {
1348
        $formula = trim($formula);
1349
 
1350
        if (empty($formula)) {
1351
            return NULL;
1352
 
1353
        }
1354
 
1355
        // normalize formula - we want grade item ids ##giXXX## instead of [[idnumber]]
1356
        if ($grade_items = grade_item::fetch_all(array('courseid'=>$courseid))) {
1357
            foreach ($grade_items as $grade_item) {
1358
                $formula = str_replace('[['.$grade_item->idnumber.']]', '##gi'.$grade_item->id.'##', $formula);
1359
            }
1360
        }
1361
 
1362
        return $formula;
1363
    }
1364
 
1365
    /**
1366
     * Returns the final values for this grade item (as imported by module or other source).
1367
     *
1368
     * @param int $userid Optional: to retrieve a single user's final grade
1369
     * @return array|grade_grade An array of all grade_grade instances for this grade_item, or a single grade_grade instance.
1370
     */
1371
    public function get_final($userid=NULL) {
1372
        global $DB;
1373
        if ($userid) {
1374
            if ($user = $DB->get_record('grade_grades', array('itemid' => $this->id, 'userid' => $userid))) {
1375
                return $user;
1376
            }
1377
 
1378
        } else {
1379
            if ($grades = $DB->get_records('grade_grades', array('itemid' => $this->id))) {
1380
                //TODO: speed up with better SQL (MDL-31380)
1381
                $result = array();
1382
                foreach ($grades as $grade) {
1383
                    $result[$grade->userid] = $grade;
1384
                }
1385
                return $result;
1386
            } else {
1387
                return array();
1388
            }
1389
        }
1390
    }
1391
 
1392
    /**
1393
     * Get (or create if not exist yet) grade for this user
1394
     *
1395
     * @param int $userid The user ID
1396
     * @param bool $create If true and the user has no grade for this grade item a new grade_grade instance will be inserted
1397
     * @return grade_grade The grade_grade instance for the user for this grade item
1398
     */
1399
    public function get_grade($userid, $create=true) {
1400
        if (empty($this->id)) {
1401
            debugging('Can not use before insert');
1402
            return false;
1403
        }
1404
 
1405
        $grade = new grade_grade(array('userid'=>$userid, 'itemid'=>$this->id));
1406
        if (empty($grade->id) and $create) {
1407
            $grade->insert();
1408
        }
1409
 
1410
        return $grade;
1411
    }
1412
 
1413
    /**
1414
     * Returns the sortorder of this grade_item. This method is also available in
1415
     * grade_category, for cases where the object type is not know.
1416
     *
1417
     * @return int Sort order
1418
     */
1419
    public function get_sortorder() {
1420
        return $this->sortorder;
1421
    }
1422
 
1423
    /**
1424
     * Returns the idnumber of this grade_item. This method is also available in
1425
     * grade_category, for cases where the object type is not know.
1426
     *
1427
     * @return string The grade item idnumber
1428
     */
1429
    public function get_idnumber() {
1430
        return $this->idnumber;
1431
    }
1432
 
1433
    /**
1434
     * Returns this grade_item. This method is also available in
1435
     * grade_category, for cases where the object type is not know.
1436
     *
1437
     * @return grade_item
1438
     */
1439
    public function get_grade_item() {
1440
        return $this;
1441
    }
1442
 
1443
    /**
1444
     * Sets the sortorder of this grade_item. This method is also available in
1445
     * grade_category, for cases where the object type is not know.
1446
     *
1447
     * @param int $sortorder
1448
     */
1449
    public function set_sortorder($sortorder) {
1450
        if ($this->sortorder == $sortorder) {
1451
            return;
1452
        }
1453
        $this->sortorder = $sortorder;
1454
        $this->update();
1455
    }
1456
 
1457
    /**
1458
     * Update this grade item's sortorder so that it will appear after $sortorder
1459
     *
1460
     * @param int $sortorder The sort order to place this grade item after
1461
     */
1462
    public function move_after_sortorder($sortorder) {
1463
        global $CFG, $DB;
1464
 
1465
        //make some room first
1466
        $params = array($sortorder, $this->courseid);
1467
        $sql = "UPDATE {grade_items}
1468
                   SET sortorder = sortorder + 1
1469
                 WHERE sortorder > ? AND courseid = ?";
1470
        $DB->execute($sql, $params);
1471
 
1472
        $this->set_sortorder($sortorder + 1);
1473
    }
1474
 
1475
    /**
1476
     * Detect duplicate grade item's sortorder and re-sort them.
1477
     * Note: Duplicate sortorder will be introduced while duplicating activities or
1478
     * merging two courses.
1479
     *
1480
     * @param int $courseid id of the course for which grade_items sortorder need to be fixed.
1481
     */
1482
    public static function fix_duplicate_sortorder($courseid) {
1483
        global $DB;
1484
 
1485
        $transaction = $DB->start_delegated_transaction();
1486
 
1487
        $sql = "SELECT DISTINCT g1.id, g1.courseid, g1.sortorder
1488
                    FROM {grade_items} g1
1489
                    JOIN {grade_items} g2 ON g1.courseid = g2.courseid
1490
                WHERE g1.sortorder = g2.sortorder AND g1.id != g2.id AND g1.courseid = :courseid
1491
                ORDER BY g1.sortorder DESC, g1.id DESC";
1492
 
1493
        // Get all duplicates in course highest sort order, and higest id first so that we can make space at the
1494
        // bottom higher end of the sort orders and work down by id.
1495
        $rs = $DB->get_recordset_sql($sql, array('courseid' => $courseid));
1496
 
1497
        foreach($rs as $duplicate) {
1498
            $DB->execute("UPDATE {grade_items}
1499
                            SET sortorder = sortorder + 1
1500
                          WHERE courseid = :courseid AND
1501
                          (sortorder > :sortorder OR (sortorder = :sortorder2 AND id > :id))",
1502
                array('courseid' => $duplicate->courseid,
1503
                    'sortorder' => $duplicate->sortorder,
1504
                    'sortorder2' => $duplicate->sortorder,
1505
                    'id' => $duplicate->id));
1506
        }
1507
        $rs->close();
1508
        $transaction->allow_commit();
1509
    }
1510
 
1511
    /**
1512
     * Returns the most descriptive field for this object.
1513
     *
1514
     * Determines what type of grade item it is then returns the appropriate string
1515
     *
1516
     * @param bool $fulltotal If the item is a category total, returns $categoryname."total" instead of "Category total" or "Course total"
1517
     * @param bool $escape Whether the returned category name is to be HTML escaped or not.
1518
     * @return string name
1519
     */
1520
    public function get_name($fulltotal=false, $escape = true) {
1521
        global $CFG;
1522
        require_once($CFG->dirroot . '/course/lib.php');
1523
        if (strval($this->itemname) !== '') {
1524
            // MDL-10557
1525
 
1526
            // Make it obvious to users if the course module to which this grade item relates, is currently being removed.
1527
            $deletionpending = course_module_instance_pending_deletion($this->courseid, $this->itemmodule, $this->iteminstance);
1528
            $deletionnotice = get_string('gradesmoduledeletionprefix', 'grades');
1529
 
1530
            $options = ['context' => context_course::instance($this->courseid), 'escape' => $escape];
1531
            return $deletionpending ?
1532
                format_string($deletionnotice . ' ' . $this->itemname, true, $options) :
1533
                format_string($this->itemname, true, $options);
1534
 
1535
        } else if ($this->is_course_item()) {
1536
            return get_string('coursetotal', 'grades');
1537
 
1538
        } else if ($this->is_category_item()) {
1539
            if ($fulltotal) {
1540
                $category = $this->load_parent_category();
1541
                $a = new stdClass();
1542
                $a->category = $category->get_name($escape);
1543
                return get_string('categorytotalfull', 'grades', $a);
1544
            } else {
1545
            return get_string('categorytotal', 'grades');
1546
            }
1547
 
1548
        } else {
1549
            return get_string('gradenoun');
1550
        }
1551
    }
1552
 
1553
    /**
1554
     * A grade item can return a more detailed description which will be added to the header of the column/row in some reports.
1555
     *
1556
     * @return string description
1557
     */
1558
    public function get_description() {
1559
        if ($this->is_course_item() || $this->is_category_item()) {
1560
            $categoryitem = $this->load_item_category();
1561
            return $categoryitem->get_description();
1562
        }
1563
        return '';
1564
    }
1565
 
1566
    /**
1567
     * Sets this item's categoryid. A generic method shared by objects that have a parent id of some kind.
1568
     *
1569
     * @param int $parentid The ID of the new parent
1570
     * @param bool $updateaggregationfields Whether or not to convert the aggregation fields when switching between category.
1571
     *                          Set this to false when the aggregation fields have been updated in prevision of the new
1572
     *                          category, typically when the item is freshly created.
1573
     * @return bool True if success
1574
     */
1575
    public function set_parent($parentid, $updateaggregationfields = true) {
1576
        if ($this->is_course_item() or $this->is_category_item()) {
1577
            throw new \moodle_exception('cannotsetparentforcatoritem');
1578
        }
1579
 
1580
        if ($this->categoryid == $parentid) {
1581
            return true;
1582
        }
1583
 
1584
        // find parent and check course id
1585
        if (!$parent_category = grade_category::fetch(array('id'=>$parentid, 'courseid'=>$this->courseid))) {
1586
            return false;
1587
        }
1588
 
1589
        $currentparent = $this->load_parent_category();
1590
 
1591
        if ($updateaggregationfields) {
1592
            $this->set_aggregation_fields_for_aggregation($currentparent->aggregation, $parent_category->aggregation);
1593
        }
1594
 
1595
        $this->force_regrading();
1596
 
1597
        // set new parent
1598
        $this->categoryid = $parent_category->id;
1599
        $this->parent_category =& $parent_category;
1600
 
1601
        return $this->update();
1602
    }
1603
 
1604
    /**
1605
     * Update the aggregation fields when the aggregation changed.
1606
     *
1607
     * This method should always be called when the aggregation has changed, but also when
1608
     * the item was moved to another category, even it if uses the same aggregation method.
1609
     *
1610
     * Some values such as the weight only make sense within a category, once moved the
1611
     * values should be reset to let the user adapt them accordingly.
1612
     *
1613
     * Note that this method does not save the grade item.
1614
     * {@link grade_item::update()} has to be called manually after using this method.
1615
     *
1616
     * @param  int $from Aggregation method constant value.
1617
     * @param  int $to   Aggregation method constant value.
1618
     * @return boolean   True when at least one field was changed, false otherwise
1619
     */
1620
    public function set_aggregation_fields_for_aggregation($from, $to) {
1621
        $defaults = grade_category::get_default_aggregation_coefficient_values($to);
1622
 
1623
        $origaggregationcoef = $this->aggregationcoef;
1624
        $origaggregationcoef2 = $this->aggregationcoef2;
1625
        $origweighoverride = $this->weightoverride;
1626
 
1627
        if ($from == GRADE_AGGREGATE_SUM && $to == GRADE_AGGREGATE_SUM && $this->weightoverride) {
1628
            // Do nothing. We are switching from SUM to SUM and the weight is overriden,
1629
            // a teacher would not expect any change in this situation.
1630
 
1631
        } else if ($from == GRADE_AGGREGATE_WEIGHTED_MEAN && $to == GRADE_AGGREGATE_WEIGHTED_MEAN) {
1632
            // Do nothing. The weights can be kept in this case.
1633
 
1634
        } else if (in_array($from, array(GRADE_AGGREGATE_SUM,  GRADE_AGGREGATE_EXTRACREDIT_MEAN, GRADE_AGGREGATE_WEIGHTED_MEAN2))
1635
                && in_array($to, array(GRADE_AGGREGATE_SUM,  GRADE_AGGREGATE_EXTRACREDIT_MEAN, GRADE_AGGREGATE_WEIGHTED_MEAN2))) {
1636
 
1637
            // Reset all but the the extra credit field.
1638
            $this->aggregationcoef2 = $defaults['aggregationcoef2'];
1639
            $this->weightoverride = $defaults['weightoverride'];
1640
 
1641
            if ($to != GRADE_AGGREGATE_EXTRACREDIT_MEAN) {
1642
                // Normalise extra credit, except for 'Mean with extra credit' which supports higher values than 1.
1643
                $this->aggregationcoef = min(1, $this->aggregationcoef);
1644
            }
1645
        } else {
1646
            // Reset all.
1647
            $this->aggregationcoef = $defaults['aggregationcoef'];
1648
            $this->aggregationcoef2 = $defaults['aggregationcoef2'];
1649
            $this->weightoverride = $defaults['weightoverride'];
1650
        }
1651
 
1652
        $acoefdiff       = grade_floats_different($origaggregationcoef, $this->aggregationcoef);
1653
        $acoefdiff2      = grade_floats_different($origaggregationcoef2, $this->aggregationcoef2);
1654
        $weightoverride  = grade_floats_different($origweighoverride, $this->weightoverride);
1655
 
1656
        return $acoefdiff || $acoefdiff2 || $weightoverride;
1657
    }
1658
 
1659
    /**
1660
     * Makes sure value is a valid grade value.
1661
     *
1662
     * @param float $gradevalue
1663
     * @return mixed float or int fixed grade value
1664
     */
1665
    public function bounded_grade($gradevalue) {
1666
        global $CFG;
1667
 
1668
        if (is_null($gradevalue)) {
1669
            return null;
1670
        }
1671
 
1672
        if ($this->gradetype == GRADE_TYPE_SCALE) {
1673
            // no >100% grades hack for scale grades!
1674
            // 1.5 is rounded to 2 ;-)
1675
            return (int)bounded_number($this->grademin, round($gradevalue+0.00001), $this->grademax);
1676
        }
1677
 
1678
        $grademax = $this->grademax;
1679
 
1680
        // NOTE: if you change this value you must manually reset the needsupdate flag in all grade items
1681
        $maxcoef = isset($CFG->gradeoverhundredprocentmax) ? $CFG->gradeoverhundredprocentmax : 10; // 1000% max by default
1682
 
1683
        if (!empty($CFG->unlimitedgrades)) {
1684
            // NOTE: if you change this value you must manually reset the needsupdate flag in all grade items
1685
            $grademax = $grademax * $maxcoef;
1686
        } else if ($this->is_category_item() or $this->is_course_item()) {
1687
            $category = $this->load_item_category();
1688
            if ($category->aggregation >= 100) {
1689
                // grade >100% hack
1690
                $grademax = $grademax * $maxcoef;
1691
            }
1692
        }
1693
 
1694
        return (float)bounded_number($this->grademin, $gradevalue, $grademax);
1695
    }
1696
 
1697
    /**
1698
     * Finds out on which other items does this depend directly when doing calculation or category aggregation
1699
     *
1700
     * @param bool $reset_cache
1701
     * @return array of grade_item IDs this one depends on
1702
     */
1703
    public function depends_on($reset_cache=false) {
1704
        global $CFG, $DB;
1705
 
1706
        if ($reset_cache) {
1707
            $this->dependson_cache = null;
1708
        } else if (isset($this->dependson_cache)) {
1709
            return $this->dependson_cache;
1710
        }
1711
 
1712
        if ($this->is_locked() && !$this->is_category_item()) {
1713
            // locked items do not need to be regraded
1714
            $this->dependson_cache = array();
1715
            return $this->dependson_cache;
1716
        }
1717
 
1718
        if ($this->is_calculated()) {
1719
            if (preg_match_all('/##gi(\d+)##/', $this->calculation, $matches)) {
1720
                $this->dependson_cache = array_unique($matches[1]); // remove duplicates
1721
                return $this->dependson_cache;
1722
            } else {
1723
                $this->dependson_cache = array();
1724
                return $this->dependson_cache;
1725
            }
1726
 
1727
        } else if ($grade_category = $this->load_item_category()) {
1728
            $params = array();
1729
 
1730
            //only items with numeric or scale values can be aggregated
1441 ariadna 1731
            if (!$this->is_gradable()) {
1 efrain 1732
                $this->dependson_cache = array();
1733
                return $this->dependson_cache;
1734
            }
1735
 
1736
            $grade_category->apply_forced_settings();
1737
 
1738
            if (empty($CFG->enableoutcomes) or $grade_category->aggregateoutcomes) {
1739
                $outcomes_sql = "";
1740
            } else {
1741
                $outcomes_sql = "AND gi.outcomeid IS NULL";
1742
            }
1743
 
1744
            if (empty($CFG->grade_includescalesinaggregation)) {
1745
                $gtypes = "gi.gradetype = ?";
1746
                $params[] = GRADE_TYPE_VALUE;
1747
            } else {
1748
                $gtypes = "(gi.gradetype = ? OR gi.gradetype = ?)";
1749
                $params[] = GRADE_TYPE_VALUE;
1750
                $params[] = GRADE_TYPE_SCALE;
1751
            }
1752
 
1753
            $params[] = $grade_category->id;
1754
            $params[] = $this->courseid;
1755
            $params[] = $grade_category->id;
1756
            $params[] = $this->courseid;
1757
            if (empty($CFG->grade_includescalesinaggregation)) {
1758
                $params[] = GRADE_TYPE_VALUE;
1759
            } else {
1760
                $params[] = GRADE_TYPE_VALUE;
1761
                $params[] = GRADE_TYPE_SCALE;
1762
            }
1763
            $sql = "SELECT gi.id
1764
                      FROM {grade_items} gi
1765
                     WHERE $gtypes
1766
                           AND gi.categoryid = ?
1767
                           AND gi.courseid = ?
1768
                           $outcomes_sql
1769
                    UNION
1770
 
1771
                    SELECT gi.id
1772
                      FROM {grade_items} gi, {grade_categories} gc
1773
                     WHERE (gi.itemtype = 'category' OR gi.itemtype = 'course') AND gi.iteminstance=gc.id
1774
                           AND gc.parent = ?
1775
                           AND gi.courseid = ?
1776
                           AND $gtypes
1777
                           $outcomes_sql";
1778
 
1779
            if ($children = $DB->get_records_sql($sql, $params)) {
1780
                $this->dependson_cache = array_keys($children);
1781
                return $this->dependson_cache;
1782
            } else {
1783
                $this->dependson_cache = array();
1784
                return $this->dependson_cache;
1785
            }
1786
 
1787
        } else {
1788
            $this->dependson_cache = array();
1789
            return $this->dependson_cache;
1790
        }
1791
    }
1792
 
1793
    /**
1794
     * Refetch grades from modules, plugins.
1795
     *
1796
     * @param int $userid optional, limit the refetch to a single user
1797
     * @return bool Returns true on success or if there is nothing to do
1798
     */
1799
    public function refresh_grades($userid=0) {
1800
        global $DB;
1801
        if ($this->itemtype == 'mod') {
1802
            if ($this->is_outcome_item()) {
1803
                //nothing to do
1804
                return true;
1805
            }
1806
 
1807
            if (!$activity = $DB->get_record($this->itemmodule, array('id' => $this->iteminstance))) {
1808
                debugging("Can not find $this->itemmodule activity with id $this->iteminstance");
1809
                return false;
1810
            }
1811
 
1812
            if (!$cm = get_coursemodule_from_instance($this->itemmodule, $activity->id, $this->courseid)) {
1813
                debugging('Can not find course module');
1814
                return false;
1815
            }
1816
 
1817
            $activity->modname    = $this->itemmodule;
1818
            $activity->cmidnumber = $cm->idnumber;
1819
 
1820
            return grade_update_mod_grades($activity, $userid);
1821
        }
1822
 
1823
        return true;
1824
    }
1825
 
1826
    /**
1827
     * Updates final grade value for given user, this is a only way to update final
1828
     * grades from gradebook and import because it logs the change in history table
1829
     * and deals with overridden flag. This flag is set to prevent later overriding
1830
     * from raw grades submitted from modules.
1831
     *
1832
     * @param int $userid The graded user
1833
     * @param float|false $finalgrade The float value of final grade, false means do not change
1834
     * @param string $source The modification source
1835
     * @param string $feedback Optional teacher feedback
1836
     * @param int $feedbackformat A format like FORMAT_PLAIN or FORMAT_HTML
1837
     * @param int $usermodified The ID of the user making the modification
1838
     * @param int $timemodified Optional parameter to set the time modified, if not present current time.
1839
     * @param bool $isbulkupdate If bulk grade update is happening.
1840
     * @return bool success
1841
     */
1842
    public function update_final_grade($userid, $finalgrade = false, $source = null, $feedback = false,
1843
            $feedbackformat = FORMAT_MOODLE, $usermodified = null, $timemodified = null, $isbulkupdate = false) {
1844
        global $USER, $CFG;
1845
 
1846
        $result = true;
1847
 
1848
        // no grading used or locked
1849
        if ($this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) {
1850
            return false;
1851
        }
1852
 
1853
        $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid));
1854
        $grade->grade_item =& $this; // prevent db fetching of this grade_item
1855
 
1856
        if (empty($usermodified)) {
1857
            $grade->usermodified = $USER->id;
1858
        } else {
1859
            $grade->usermodified = $usermodified;
1860
        }
1861
 
1862
        if ($grade->is_locked()) {
1863
            // do not update locked grades at all
1864
            return false;
1865
        }
1866
 
1867
        $locktime = $grade->get_locktime();
1868
        if ($locktime and $locktime < time()) {
1869
            // do not update grades that should be already locked, force regrade instead
1870
            $this->force_regrading();
1871
            return false;
1872
        }
1873
 
1874
        $oldgrade = new stdClass();
1875
        $oldgrade->finalgrade     = $grade->finalgrade;
1876
        $oldgrade->overridden     = $grade->overridden;
1877
        $oldgrade->feedback       = $grade->feedback;
1878
        $oldgrade->feedbackformat = $grade->feedbackformat;
1879
        $oldgrade->rawgrademin    = $grade->rawgrademin;
1880
        $oldgrade->rawgrademax    = $grade->rawgrademax;
1881
 
1882
        // MDL-31713 rawgramemin and max must be up to date so conditional access %'s works properly.
1883
        $grade->rawgrademin = $this->grademin;
1884
        $grade->rawgrademax = $this->grademax;
1885
        $grade->rawscaleid  = $this->scaleid;
1886
 
1887
        // changed grade?
1888
        if ($finalgrade !== false) {
1889
            if ($this->is_overridable_item() && $this->markasoverriddenwhengraded) {
1890
                $grade->overridden = time();
1891
            }
1892
 
1893
            $grade->finalgrade = $this->bounded_grade($finalgrade);
1894
        }
1895
 
1896
        // do we have comment from teacher?
1897
        if ($feedback !== false) {
1898
            if ($this->is_overridable_item_feedback()) {
1899
                // external items (modules, plugins) may have own feedback
1900
                $grade->overridden = time();
1901
            }
1902
 
1903
            $grade->feedback       = $feedback;
1904
            $grade->feedbackformat = $feedbackformat;
1905
        }
1906
 
1907
        $gradechanged = false;
1908
        if (empty($grade->id)) {
1909
            $grade->timecreated = null;   // Hack alert - date submitted - no submission yet.
1910
            $grade->timemodified = $timemodified ?? time(); // Hack alert - date graded.
1911
            $result = (bool)$grade->insert($source, $isbulkupdate);
1912
 
1913
            // If the grade insert was successful and the final grade was not null then trigger a user_graded event.
1914
            if ($result && !is_null($grade->finalgrade)) {
1915
                \core\event\user_graded::create_from_grade($grade)->trigger();
1916
            }
1917
            $gradechanged = true;
1918
        } else {
1919
            // Existing grade_grades.
1920
 
1921
            if (grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)
1922
                    or grade_floats_different($grade->rawgrademin, $oldgrade->rawgrademin)
1923
                    or grade_floats_different($grade->rawgrademax, $oldgrade->rawgrademax)
1924
                    or ($oldgrade->overridden == 0 and $grade->overridden > 0)) {
1925
                $gradechanged = true;
1926
            }
1927
 
1928
            if ($grade->feedback === $oldgrade->feedback and $grade->feedbackformat == $oldgrade->feedbackformat and
1929
                    $gradechanged === false) {
1930
                // No grade nor feedback changed.
1931
                return $result;
1932
            }
1933
 
1934
            $grade->timemodified = $timemodified ?? time(); // Hack alert - date graded.
1935
            $result = $grade->update($source, $isbulkupdate);
1936
 
1937
            // If the grade update was successful and the actual grade has changed then trigger a user_graded event.
1938
            if ($result && grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)) {
1939
                \core\event\user_graded::create_from_grade($grade)->trigger();
1940
            }
1941
        }
1942
 
1943
        if (!$result) {
1944
            // Something went wrong - better force final grade recalculation.
1945
            $this->force_regrading();
1946
            return $result;
1947
        }
1948
 
1949
        // If we are not updating grades we don't need to recalculate the whole course.
1950
        if (!$gradechanged) {
1951
            return $result;
1952
        }
1953
 
1954
        if ($this->is_course_item() and !$this->needsupdate) {
1955
            if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
1956
                $this->force_regrading();
1957
            }
1958
 
1959
        } else if (!$this->needsupdate) {
1960
 
1961
            $course_item = grade_item::fetch_course_item($this->courseid);
1962
            if (!$course_item->needsupdate) {
1963
                if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
1964
                    $this->force_regrading();
1965
                }
1966
            } else {
1967
                $this->force_regrading();
1968
            }
1969
        }
1970
 
1971
        return $result;
1972
    }
1973
 
1974
 
1975
    /**
1976
     * Updates raw grade value for given user, this is a only way to update raw
1977
     * grades from external source (modules, etc.),
1978
     * because it logs the change in history table and deals with final grade recalculation.
1979
     *
1980
     * @param int $userid the graded user
1981
     * @param mixed $rawgrade float value of raw grade - false means do not change
1982
     * @param string $source modification source
1983
     * @param string $feedback optional teacher feedback
1984
     * @param int $feedbackformat A format like FORMAT_PLAIN or FORMAT_HTML
1985
     * @param int $usermodified the ID of the user who did the grading
1986
     * @param int $dategraded A timestamp of when the student's work was graded
1987
     * @param int $datesubmitted A timestamp of when the student's work was submitted
1988
     * @param grade_grade $grade A grade object, useful for bulk upgrades
1989
     * @param array $feedbackfiles An array identifying the location of files we want to copy to the gradebook feedback area.
1990
     *        Example -
1991
     *        [
1992
     *            'contextid' => 1,
1993
     *            'component' => 'mod_xyz',
1994
     *            'filearea' => 'mod_xyz_feedback',
1995
     *            'itemid' => 2
1996
     *        ];
1997
     * @param bool $isbulkupdate If bulk grade update is happening.
1998
     * @return bool success
1999
     */
2000
    public function update_raw_grade($userid, $rawgrade = false, $source = null, $feedback = false,
2001
            $feedbackformat = FORMAT_MOODLE, $usermodified = null, $dategraded = null, $datesubmitted=null,
2002
            $grade = null, array $feedbackfiles = [], $isbulkupdate = false) {
2003
        global $USER;
2004
 
2005
        $result = true;
2006
 
2007
        // calculated grades can not be updated; course and category can not be updated  because they are aggregated
2008
        if (!$this->is_raw_used() or $this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) {
2009
            return false;
2010
        }
2011
 
2012
        if (is_null($grade)) {
2013
            //fetch from db
2014
            $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid));
2015
        }
2016
        $grade->grade_item =& $this; // prevent db fetching of this grade_item
2017
 
2018
        if (empty($usermodified)) {
2019
            $grade->usermodified = $USER->id;
2020
        } else {
2021
            $grade->usermodified = $usermodified;
2022
        }
2023
 
2024
        if ($grade->is_locked()) {
2025
            // do not update locked grades at all
2026
            return false;
2027
        }
2028
 
2029
        $locktime = $grade->get_locktime();
2030
        if ($locktime and $locktime < time()) {
2031
            // do not update grades that should be already locked and force regrade
2032
            $this->force_regrading();
2033
            return false;
2034
        }
2035
 
2036
        $oldgrade = new stdClass();
2037
        $oldgrade->finalgrade     = $grade->finalgrade;
2038
        $oldgrade->rawgrade       = $grade->rawgrade;
2039
        $oldgrade->rawgrademin    = $grade->rawgrademin;
2040
        $oldgrade->rawgrademax    = $grade->rawgrademax;
2041
        $oldgrade->rawscaleid     = $grade->rawscaleid;
2042
        $oldgrade->feedback       = $grade->feedback;
2043
        $oldgrade->feedbackformat = $grade->feedbackformat;
2044
 
2045
        // use new min and max
2046
        $grade->rawgrade    = $grade->rawgrade;
2047
        $grade->rawgrademin = $this->grademin;
2048
        $grade->rawgrademax = $this->grademax;
2049
        $grade->rawscaleid  = $this->scaleid;
2050
 
2051
        // change raw grade?
2052
        if ($rawgrade !== false) {
2053
            $grade->rawgrade = $rawgrade;
2054
        }
2055
 
2056
        // empty feedback means no feedback at all
2057
        if ($feedback === '') {
2058
            $feedback = null;
2059
        }
2060
 
2061
        // do we have comment from teacher?
2062
        if ($feedback !== false and !$grade->is_overridden()) {
2063
            $grade->feedback       = $feedback;
2064
            $grade->feedbackformat = $feedbackformat;
2065
            $grade->feedbackfiles  = $feedbackfiles;
2066
        }
2067
 
2068
        // update final grade if possible
2069
        if (!$grade->is_locked() and !$grade->is_overridden()) {
2070
            $grade->finalgrade = $this->adjust_raw_grade($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
2071
        }
2072
 
2073
        // TODO: hack alert - create new fields for these in 2.0
2074
        $oldgrade->timecreated  = $grade->timecreated;
2075
        $oldgrade->timemodified = $grade->timemodified;
2076
 
2077
        $grade->timecreated = $datesubmitted;
2078
 
2079
        if ($grade->is_overridden()) {
2080
            // keep original graded date - update_final_grade() sets this for overridden grades
2081
 
2082
        } else if (is_null($grade->rawgrade) and is_null($grade->feedback)) {
2083
            // no grade and feedback means no grading yet
2084
            $grade->timemodified = null;
2085
 
2086
        } else if (!empty($dategraded)) {
2087
            // fine - module sends info when graded (yay!)
2088
            $grade->timemodified = $dategraded;
2089
 
2090
        } else if (grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)
2091
                   or $grade->feedback !== $oldgrade->feedback) {
2092
            // guess - if either grade or feedback changed set new graded date
2093
            $grade->timemodified = time();
2094
 
2095
        } else {
2096
            //keep original graded date
2097
        }
2098
        // end of hack alert
2099
 
1441 ariadna 2100
        // Only reset the deducted mark if the grade has changed.
2101
        if ($grade->timemodified !== $oldgrade->timemodified) {
2102
            $grade->deductedmark = 0;
2103
        }
2104
 
1 efrain 2105
        $gradechanged = false;
2106
        if (empty($grade->id)) {
2107
            $result = (bool)$grade->insert($source, $isbulkupdate);
2108
 
2109
            // If the grade insert was successful and the final grade was not null then trigger a user_graded event.
2110
            if ($result && !is_null($grade->finalgrade)) {
2111
                \core\event\user_graded::create_from_grade($grade)->trigger();
2112
            }
2113
            $gradechanged = true;
2114
        } else {
2115
            // Existing grade_grades.
2116
 
2117
            if (grade_floats_different($grade->finalgrade,  $oldgrade->finalgrade)
2118
                    or grade_floats_different($grade->rawgrade,    $oldgrade->rawgrade)
2119
                    or grade_floats_different($grade->rawgrademin, $oldgrade->rawgrademin)
2120
                    or grade_floats_different($grade->rawgrademax, $oldgrade->rawgrademax)
2121
                    or $grade->rawscaleid != $oldgrade->rawscaleid) {
2122
                $gradechanged = true;
2123
            }
2124
 
2125
            // The timecreated and timemodified checking is part of the hack above.
2126
            if ($gradechanged === false and
2127
                    $grade->feedback === $oldgrade->feedback and
2128
                    $grade->feedbackformat == $oldgrade->feedbackformat and
2129
                    $grade->timecreated == $oldgrade->timecreated and
2130
                    $grade->timemodified == $oldgrade->timemodified) {
2131
                // No changes.
2132
                return $result;
2133
            }
2134
            $result = $grade->update($source, $isbulkupdate);
2135
 
2136
            // If the grade update was successful and the actual grade has changed then trigger a user_graded event.
2137
            if ($result && grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)) {
2138
                \core\event\user_graded::create_from_grade($grade)->trigger();
2139
            }
2140
        }
2141
 
2142
        if (!$result) {
2143
            // Something went wrong - better force final grade recalculation.
2144
            $this->force_regrading();
2145
            return $result;
2146
        }
2147
 
2148
        // If we are not updating grades we don't need to recalculate the whole course.
2149
        if (!$gradechanged) {
2150
            return $result;
2151
        }
2152
 
2153
        if (!$this->needsupdate) {
2154
            $course_item = grade_item::fetch_course_item($this->courseid);
2155
            if (!$course_item->needsupdate) {
2156
                if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
2157
                    $this->force_regrading();
2158
                }
2159
            }
2160
        }
2161
 
2162
        return $result;
2163
    }
2164
 
2165
    /**
1441 ariadna 2166
     * Update penalty value for given user
2167
     *
2168
     * @param int $userid The graded user
2169
     * @param float $deductedmark The mark deducted from final grade
2170
     */
2171
    public function update_deducted_mark(int $userid, float $deductedmark): void {
2172
        $grade = new grade_grade([
2173
                'itemid' => $this->id,
2174
                'userid' => $userid,
2175
            ]);
2176
        $grade->deductedmark = $deductedmark;
2177
        $grade->update();
2178
    }
2179
 
2180
    /**
1 efrain 2181
     * Calculates final grade values using the formula in the calculation property.
2182
     * The parameters are taken from final grades of grade items in current course only.
2183
     *
2184
     * @param int $userid Supply a user ID to limit the calculations to the grades of a single user
2185
     * @return bool false if error
2186
     */
2187
    public function compute($userid=null) {
2188
        global $CFG, $DB;
2189
 
2190
        if (!$this->is_calculated()) {
2191
            return false;
2192
        }
2193
 
2194
        require_once($CFG->libdir.'/mathslib.php');
2195
 
2196
        if ($this->is_locked()) {
2197
            return true; // no need to recalculate locked items
2198
        }
2199
 
2200
        // Precreate grades - we need them to exist
2201
        if ($userid) {
2202
            $missing = array();
2203
            if (!$DB->record_exists('grade_grades', array('itemid'=>$this->id, 'userid'=>$userid))) {
2204
                $m = new stdClass();
2205
                $m->userid = $userid;
2206
                $missing[] = $m;
2207
            }
2208
        } else {
2209
            // Find any users who have grades for some but not all grade items in this course
2210
            $params = array('gicourseid' => $this->courseid, 'ggitemid' => $this->id);
2211
            $sql = "SELECT gg.userid
2212
                      FROM {grade_grades} gg
2213
                           JOIN {grade_items} gi
2214
                           ON (gi.id = gg.itemid AND gi.courseid = :gicourseid)
2215
                     GROUP BY gg.userid
2216
                     HAVING SUM(CASE WHEN gg.itemid = :ggitemid THEN 1 ELSE 0 END) = 0";
2217
            $missing = $DB->get_records_sql($sql, $params);
2218
        }
2219
 
2220
        if ($missing) {
2221
            foreach ($missing as $m) {
2222
                $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$m->userid), false);
2223
                $grade->grade_item =& $this;
2224
                $grade->insert('system');
2225
            }
2226
        }
2227
 
2228
        // get used items
2229
        $useditems = $this->depends_on();
2230
 
2231
        // prepare formula and init maths library
2232
        $formula = preg_replace('/##(gi\d+)##/', '\1', $this->calculation);
2233
        if (strpos($formula, '[[') !== false) {
2234
            // missing item
2235
            return false;
2236
        }
2237
        $this->formula = new calc_formula($formula);
2238
 
2239
        // where to look for final grades?
2240
        // this itemid is added so that we use only one query for source and final grades
2241
        $gis = array_merge($useditems, array($this->id));
2242
        list($usql, $params) = $DB->get_in_or_equal($gis);
2243
 
2244
        if ($userid) {
1441 ariadna 2245
            $usersql = "AND userid=?";
1 efrain 2246
            $params[] = $userid;
2247
        } else {
2248
            $usersql = "";
2249
        }
2250
 
1441 ariadna 2251
        $gradeinst = new grade_grade();
2252
        $fields = implode(',', $gradeinst->required_fields);
1 efrain 2253
 
2254
        $params[] = $this->courseid;
2255
 
2256
        $return = true;
2257
 
2258
        // group the grades by userid and use formula on the group
1441 ariadna 2259
        $rs = $DB->get_recordset_select('grade_grades', "itemid $usql $usersql", $params, 'userid', $fields);
1 efrain 2260
        if ($rs->valid()) {
2261
            $prevuser = 0;
2262
            $grade_records   = array();
2263
            $oldgrade    = null;
2264
            foreach ($rs as $used) {
2265
                if ($used->userid != $prevuser) {
2266
                    if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {
2267
                        $return = false;
2268
                    }
2269
                    $prevuser = $used->userid;
2270
                    $grade_records   = array();
2271
                    $oldgrade    = null;
2272
                }
2273
                if ($used->itemid == $this->id) {
2274
                    $oldgrade = $used;
2275
                }
2276
                $grade_records['gi'.$used->itemid] = $used->finalgrade;
2277
            }
2278
            if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {
2279
                $return = false;
2280
            }
2281
        }
2282
        $rs->close();
2283
 
2284
        return $return;
2285
    }
2286
 
2287
    /**
2288
     * Internal function that does the final grade calculation
2289
     *
2290
     * @param int $userid The user ID
2291
     * @param array $params An array of grade items of the form {'gi'.$itemid]} => $finalgrade
2292
     * @param array $useditems An array of grade item IDs that this grade item depends on plus its own ID
2293
     * @param grade_grade $oldgrade A grade_grade instance containing the old values from the database
2294
     * @return bool False if an error occurred
2295
     */
2296
    public function use_formula($userid, $params, $useditems, $oldgrade) {
2297
        if (empty($userid)) {
2298
            return true;
2299
        }
2300
 
2301
        // add missing final grade values
2302
        // not graded (null) is counted as 0 - the spreadsheet way
2303
        $allinputsnull = true;
2304
        foreach($useditems as $gi) {
2305
            if (!array_key_exists('gi'.$gi, $params) || is_null($params['gi'.$gi])) {
2306
                $params['gi'.$gi] = 0;
2307
            } else {
2308
                $params['gi'.$gi] = (float)$params['gi'.$gi];
2309
                if ($gi != $this->id) {
2310
                    $allinputsnull = false;
2311
                }
2312
            }
2313
        }
2314
 
2315
        // can not use own final grade during calculation
2316
        unset($params['gi'.$this->id]);
2317
 
2318
        // Check to see if the gradebook is frozen. This allows grades to not be altered at all until a user verifies that they
2319
        // wish to update the grades.
2320
        $gradebookcalculationsfreeze = get_config('core', 'gradebook_calculations_freeze_' . $this->courseid);
2321
 
2322
        $rawminandmaxchanged = false;
2323
        // insert final grade - will be needed later anyway
2324
        if ($oldgrade) {
2325
            // Only run through this code if the gradebook isn't frozen.
2326
            if ($gradebookcalculationsfreeze && (int)$gradebookcalculationsfreeze <= 20150627) {
2327
                // Do nothing.
2328
            } else {
2329
                // The grade_grade for a calculated item should have the raw grade maximum and minimum set to the
2330
                // grade_item grade maximum and minimum respectively.
2331
                if ($oldgrade->rawgrademax != $this->grademax || $oldgrade->rawgrademin != $this->grademin) {
2332
                    $rawminandmaxchanged = true;
2333
                    $oldgrade->rawgrademax = $this->grademax;
2334
                    $oldgrade->rawgrademin = $this->grademin;
2335
                }
2336
            }
2337
            $oldfinalgrade = $oldgrade->finalgrade;
2338
            $grade = new grade_grade($oldgrade, false); // fetching from db is not needed
2339
            $grade->grade_item =& $this;
2340
 
2341
        } else {
2342
            $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid), false);
2343
            $grade->grade_item =& $this;
2344
            $rawminandmaxchanged = false;
2345
            if ($gradebookcalculationsfreeze && (int)$gradebookcalculationsfreeze <= 20150627) {
2346
                // Do nothing.
2347
            } else {
2348
                // The grade_grade for a calculated item should have the raw grade maximum and minimum set to the
2349
                // grade_item grade maximum and minimum respectively.
2350
                $rawminandmaxchanged = true;
2351
                $grade->rawgrademax = $this->grademax;
2352
                $grade->rawgrademin = $this->grademin;
2353
            }
2354
            $grade->insert('system');
2355
            $oldfinalgrade = null;
2356
        }
2357
 
2358
        // no need to recalculate locked or overridden grades
2359
        if ($grade->is_locked() or $grade->is_overridden()) {
2360
            return true;
2361
        }
2362
 
2363
        if ($allinputsnull) {
2364
            $grade->finalgrade = null;
2365
            $result = true;
2366
 
2367
        } else {
2368
 
2369
            // do the calculation
2370
            $this->formula->set_params($params);
2371
            $result = $this->formula->evaluate();
2372
 
2373
            if ($result === false) {
2374
                $grade->finalgrade = null;
2375
 
2376
            } else {
2377
                // normalize
2378
                $grade->finalgrade = $this->bounded_grade($result);
2379
            }
2380
        }
2381
 
2382
        // Only run through this code if the gradebook isn't frozen.
2383
        if ($gradebookcalculationsfreeze && (int)$gradebookcalculationsfreeze <= 20150627) {
2384
            // Update in db if changed.
2385
            if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) {
2386
                $grade->timemodified = time();
2387
                $success = $grade->update('compute');
2388
 
2389
                // If successful trigger a user_graded event.
2390
                if ($success) {
2391
                    \core\event\user_graded::create_from_grade($grade)->trigger();
2392
                }
2393
            }
2394
        } else {
2395
            // Update in db if changed.
2396
            if (grade_floats_different($grade->finalgrade, $oldfinalgrade) || $rawminandmaxchanged) {
2397
                $grade->timemodified = time();
2398
                $success = $grade->update('compute');
2399
 
2400
                // If successful trigger a user_graded event.
2401
                if ($success) {
2402
                    \core\event\user_graded::create_from_grade($grade)->trigger();
2403
                }
2404
            }
2405
        }
2406
 
2407
        if ($result !== false) {
2408
            //lock grade if needed
2409
        }
2410
 
2411
        if ($result === false) {
2412
            return false;
2413
        } else {
2414
            return true;
2415
        }
2416
 
2417
    }
2418
 
2419
    /**
2420
     * Validate the formula.
2421
     *
2422
     * @param string $formulastr
2423
     * @return bool true if calculation possible, false otherwise
2424
     */
2425
    public function validate_formula($formulastr) {
2426
        global $CFG, $DB;
2427
        require_once($CFG->libdir.'/mathslib.php');
2428
 
2429
        $formulastr = grade_item::normalize_formula($formulastr, $this->courseid);
2430
 
2431
        if (empty($formulastr)) {
2432
            return true;
2433
        }
2434
 
2435
        if (strpos($formulastr, '=') !== 0) {
2436
            return get_string('errorcalculationnoequal', 'grades');
2437
        }
2438
 
2439
        // get used items
2440
        if (preg_match_all('/##gi(\d+)##/', $formulastr, $matches)) {
2441
            $useditems = array_unique($matches[1]); // remove duplicates
2442
        } else {
2443
            $useditems = array();
2444
        }
2445
 
2446
        // MDL-11902
2447
        // unset the value if formula is trying to reference to itself
2448
        // but array keys does not match itemid
2449
        if (!empty($this->id)) {
2450
            $useditems = array_diff($useditems, array($this->id));
2451
            //unset($useditems[$this->id]);
2452
        }
2453
 
2454
        // prepare formula and init maths library
2455
        $formula = preg_replace('/##(gi\d+)##/', '\1', $formulastr);
2456
        $formula = new calc_formula($formula);
2457
 
2458
 
2459
        if (empty($useditems)) {
2460
            $grade_items = array();
2461
 
2462
        } else {
2463
            list($usql, $params) = $DB->get_in_or_equal($useditems);
2464
            $params[] = $this->courseid;
2465
            $sql = "SELECT gi.*
2466
                      FROM {grade_items} gi
2467
                     WHERE gi.id $usql and gi.courseid=?"; // from the same course only!
2468
 
2469
            if (!$grade_items = $DB->get_records_sql($sql, $params)) {
2470
                $grade_items = array();
2471
            }
2472
        }
2473
 
2474
        $params = array();
2475
        foreach ($useditems as $itemid) {
2476
            // make sure all grade items exist in this course
2477
            if (!array_key_exists($itemid, $grade_items)) {
2478
                return false;
2479
            }
2480
            // use max grade when testing formula, this should be ok in 99.9%
2481
            // division by 0 is one of possible problems
2482
            $params['gi'.$grade_items[$itemid]->id] = $grade_items[$itemid]->grademax;
2483
        }
2484
 
2485
        // do the calculation
2486
        $formula->set_params($params);
2487
        $result = $formula->evaluate();
2488
 
2489
        // false as result indicates some problem
2490
        if ($result === false) {
2491
            // TODO: add more error hints
2492
            return get_string('errorcalculationunknown', 'grades');
2493
        } else {
2494
            return true;
2495
        }
2496
    }
2497
 
2498
    /**
2499
     * Returns the value of the display type
2500
     *
2501
     * It can be set at 3 levels: grade_item, course setting and site. The lowest level overrides the higher ones.
2502
     *
2503
     * @return int Display type
2504
     */
2505
    public function get_displaytype() {
2506
        global $CFG;
2507
 
2508
        if ($this->display == GRADE_DISPLAY_TYPE_DEFAULT) {
2509
            return grade_get_setting($this->courseid, 'displaytype', $CFG->grade_displaytype);
2510
 
2511
        } else {
2512
            return $this->display;
2513
        }
2514
    }
2515
 
2516
    /**
2517
     * Returns the value of the decimals field
2518
     *
2519
     * It can be set at 3 levels: grade_item, course setting and site. The lowest level overrides the higher ones.
2520
     *
2521
     * @return int Decimals (0 - 5)
2522
     */
2523
    public function get_decimals() {
2524
        global $CFG;
2525
 
2526
        if (is_null($this->decimals)) {
2527
            return grade_get_setting($this->courseid, 'decimalpoints', $CFG->grade_decimalpoints);
2528
 
2529
        } else {
2530
            return $this->decimals;
2531
        }
2532
    }
2533
 
2534
    /**
2535
     * Returns a string representing the range of grademin - grademax for this grade item.
2536
     *
2537
     * @param int $rangesdisplaytype
2538
     * @param int $rangesdecimalpoints
2539
     * @return string
2540
     */
2541
    function get_formatted_range($rangesdisplaytype=null, $rangesdecimalpoints=null) {
2542
 
2543
        global $USER;
2544
 
2545
        // Determine which display type to use for this average
2546
        if (isset($USER->editing) && $USER->editing) {
2547
            $displaytype = GRADE_DISPLAY_TYPE_REAL;
2548
 
2549
        } else if ($rangesdisplaytype == GRADE_REPORT_PREFERENCE_INHERIT) { // no ==0 here, please resave report and user prefs
2550
            $displaytype = $this->get_displaytype();
2551
 
2552
        } else {
2553
            $displaytype = $rangesdisplaytype;
2554
        }
2555
 
2556
        // Override grade_item setting if a display preference (not default) was set for the averages
2557
        if ($rangesdecimalpoints == GRADE_REPORT_PREFERENCE_INHERIT) {
2558
            $decimalpoints = $this->get_decimals();
2559
 
2560
        } else {
2561
            $decimalpoints = $rangesdecimalpoints;
2562
        }
2563
 
2564
        if ($displaytype == GRADE_DISPLAY_TYPE_PERCENTAGE) {
2565
            $grademin = "0 %";
2566
            $grademax = "100 %";
2567
 
2568
        } else {
2569
            $grademin = grade_format_gradevalue($this->grademin, $this, true, $displaytype, $decimalpoints);
2570
            $grademax = grade_format_gradevalue($this->grademax, $this, true, $displaytype, $decimalpoints);
2571
        }
2572
 
2573
        return $grademin.'&ndash;'. $grademax;
2574
    }
2575
 
2576
    /**
2577
     * Queries parent categories recursively to find the aggregationcoef type that applies to this grade item.
2578
     *
2579
     * @return string|false Returns the coefficient string of false is no coefficient is being used
2580
     */
2581
    public function get_coefstring() {
2582
        $parent_category = $this->load_parent_category();
2583
        if ($this->is_category_item()) {
2584
            $parent_category = $parent_category->load_parent_category();
2585
        }
2586
 
2587
        if ($parent_category->is_aggregationcoef_used()) {
2588
            return $parent_category->get_coefstring();
2589
        } else {
2590
            return false;
2591
        }
2592
    }
2593
 
2594
    /**
2595
     * Returns whether the grade item can control the visibility of the grades
2596
     *
2597
     * @return bool
2598
     */
2599
    public function can_control_visibility() {
2600
        if (core_component::get_plugin_directory($this->itemtype, $this->itemmodule)) {
2601
            return !plugin_supports($this->itemtype, $this->itemmodule, FEATURE_CONTROLS_GRADE_VISIBILITY, false);
2602
        }
2603
        return parent::can_control_visibility();
2604
    }
2605
 
2606
    /**
2607
     * Used to notify the completion system (if necessary) that a user's grade
2608
     * has changed, and clear up a possible score cache.
2609
     *
2610
     * @param bool $deleted True if grade was actually deleted
2611
     */
2612
    protected function notify_changed($deleted) {
2613
        global $CFG;
2614
 
2615
        // Condition code may cache the grades for conditional availability of
2616
        // modules or sections. (This code should use a hook for communication
2617
        // with plugin, but hooks are not implemented at time of writing.)
2618
        if (!empty($CFG->enableavailability) && class_exists('\availability_grade\callbacks')) {
2619
            \availability_grade\callbacks::grade_item_changed($this->courseid);
2620
        }
2621
    }
2622
 
2623
    /**
2624
     * Helper function to get the accurate context for this grade column.
2625
     *
2626
     * @return context
2627
     */
2628
    public function get_context() {
2629
        if ($this->itemtype == 'mod') {
2630
            $modinfo = get_fast_modinfo($this->courseid);
2631
            // Sometimes the course module cache is out of date and needs to be rebuilt.
2632
            if (!isset($modinfo->instances[$this->itemmodule][$this->iteminstance])) {
2633
                rebuild_course_cache($this->courseid, true);
2634
                $modinfo = get_fast_modinfo($this->courseid);
2635
            }
2636
            // Even with a rebuilt cache the module does not exist. This means the
2637
            // database is in an invalid state - we will log an error and return
2638
            // the course context but the calling code should be updated.
2639
            if (!isset($modinfo->instances[$this->itemmodule][$this->iteminstance])) {
2640
                mtrace(get_string('moduleinstancedoesnotexist', 'error'));
2641
                $context = \context_course::instance($this->courseid);
2642
            } else {
2643
                $cm = $modinfo->instances[$this->itemmodule][$this->iteminstance];
2644
                $context = \context_module::instance($cm->id);
2645
            }
2646
        } else {
2647
            $context = \context_course::instance($this->courseid);
2648
        }
2649
        return $context;
2650
    }
2651
}