Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
/**
18
 * Library of functions for gradebook - both public and internal
19
 *
20
 * @package   core_grades
21
 * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
22
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
defined('MOODLE_INTERNAL') || die();
26
 
27
global $CFG;
28
 
29
/** Include essential files */
30
require_once($CFG->libdir . '/grade/constants.php');
31
 
32
require_once($CFG->libdir . '/grade/grade_category.php');
33
require_once($CFG->libdir . '/grade/grade_item.php');
34
require_once($CFG->libdir . '/grade/grade_grade.php');
35
require_once($CFG->libdir . '/grade/grade_scale.php');
36
require_once($CFG->libdir . '/grade/grade_outcome.php');
37
 
38
/////////////////////////////////////////////////////////////////////
39
///// Start of public API for communication with modules/blocks /////
40
/////////////////////////////////////////////////////////////////////
41
 
42
/**
43
 * Submit new or update grade; update/create grade_item definition. Grade must have userid specified,
44
 * rawgrade and feedback with format are optional. rawgrade NULL means 'Not graded'.
45
 * Missing property or key means does not change the existing value.
46
 *
47
 * Only following grade item properties can be changed 'itemname', 'idnumber', 'gradetype', 'grademax',
48
 * 'grademin', 'scaleid', 'multfactor', 'plusfactor', 'deleted' and 'hidden'. 'reset' means delete all current grades including locked ones.
49
 *
50
 * Manual, course or category items can not be updated by this function.
51
 *
52
 * @category grade
53
 * @param string $source Source of the grade such as 'mod/assignment'
54
 * @param int    $courseid ID of course
55
 * @param string $itemtype Type of grade item. For example, mod or block
56
 * @param string $itemmodule More specific then $itemtype. For example, assignment or forum. May be NULL for some item types
57
 * @param int    $iteminstance Instance ID of graded item
58
 * @param int    $itemnumber Most probably 0. Modules can use other numbers when having more than one grade for each user
59
 * @param mixed  $grades Grade (object, array) or several grades (arrays of arrays or objects), NULL if updating grade_item definition only
60
 * @param mixed  $itemdetails Object or array describing the grading item, NULL if no change
61
 * @param bool   $isbulkupdate If bulk grade update is happening.
62
 * @return int Returns GRADE_UPDATE_OK, GRADE_UPDATE_FAILED, GRADE_UPDATE_MULTIPLE or GRADE_UPDATE_ITEM_LOCKED
63
 */
64
function grade_update($source, $courseid, $itemtype, $itemmodule, $iteminstance, $itemnumber, $grades = null,
65
        $itemdetails = null, $isbulkupdate = false) {
66
    global $USER, $CFG, $DB;
67
 
68
    // only following grade_item properties can be changed in this function
69
    $allowed = array('itemname', 'idnumber', 'gradetype', 'grademax', 'grademin', 'scaleid', 'multfactor', 'plusfactor', 'deleted', 'hidden');
70
    // list of 10,5 numeric fields
71
    $floats  = array('grademin', 'grademax', 'multfactor', 'plusfactor');
72
 
73
    // grade item identification
74
    $params = compact('courseid', 'itemtype', 'itemmodule', 'iteminstance', 'itemnumber');
75
 
76
    if (is_null($courseid) or is_null($itemtype)) {
77
        debugging('Missing courseid or itemtype');
78
        return GRADE_UPDATE_FAILED;
79
    }
80
 
81
    if (!$gradeitems = grade_item::fetch_all($params)) {
82
        // create a new one
83
        $gradeitem = false;
84
    } else if (count($gradeitems) == 1) {
85
        $gradeitem = reset($gradeitems);
86
        unset($gradeitems); // Release memory.
87
    } else {
88
        debugging('Found more than one grade item');
89
        return GRADE_UPDATE_MULTIPLE;
90
    }
91
 
92
    if (!empty($itemdetails['deleted'])) {
93
        if ($gradeitem) {
94
            if ($gradeitem->delete($source)) {
95
                return GRADE_UPDATE_OK;
96
            } else {
97
                return GRADE_UPDATE_FAILED;
98
            }
99
        }
100
        return GRADE_UPDATE_OK;
101
    }
102
 
103
/// Create or update the grade_item if needed
104
 
105
    if (!$gradeitem) {
106
        if ($itemdetails) {
107
            $itemdetails = (array)$itemdetails;
108
 
109
            // grademin and grademax ignored when scale specified
110
            if (array_key_exists('scaleid', $itemdetails)) {
111
                if ($itemdetails['scaleid']) {
112
                    unset($itemdetails['grademin']);
113
                    unset($itemdetails['grademax']);
114
                }
115
            }
116
 
117
            foreach ($itemdetails as $k=>$v) {
118
                if (!in_array($k, $allowed)) {
119
                    // ignore it
120
                    continue;
121
                }
122
                if ($k == 'gradetype' and $v == GRADE_TYPE_NONE) {
123
                    // no grade item needed!
124
                    return GRADE_UPDATE_OK;
125
                }
126
                $params[$k] = $v;
127
            }
128
        }
129
        $gradeitem = new grade_item($params);
130
        $gradeitem->insert(null, $isbulkupdate);
131
 
132
    } else {
133
        if ($gradeitem->is_locked()) {
134
            // no notice() here, test returned value instead!
135
            return GRADE_UPDATE_ITEM_LOCKED;
136
        }
137
 
138
        if ($itemdetails) {
139
            $itemdetails = (array)$itemdetails;
140
            $update = false;
141
            foreach ($itemdetails as $k=>$v) {
142
                if (!in_array($k, $allowed)) {
143
                    // ignore it
144
                    continue;
145
                }
146
                if (in_array($k, $floats)) {
147
                    if (grade_floats_different($gradeitem->{$k}, $v)) {
148
                        $gradeitem->{$k} = $v;
149
                        $update = true;
150
                    }
151
 
152
                } else {
153
                    if ($gradeitem->{$k} != $v) {
154
                        $gradeitem->{$k} = $v;
155
                        $update = true;
156
                    }
157
                }
158
            }
159
            if ($update) {
160
                $gradeitem->update(null, $isbulkupdate);
161
            }
162
        }
163
    }
164
 
165
/// reset grades if requested
166
    if (!empty($itemdetails['reset'])) {
167
        $gradeitem->delete_all_grades('reset');
168
        return GRADE_UPDATE_OK;
169
    }
170
 
171
/// Some extra checks
172
    // do we use grading?
173
    if ($gradeitem->gradetype == GRADE_TYPE_NONE) {
174
        return GRADE_UPDATE_OK;
175
    }
176
 
177
    // no grade submitted
178
    if (empty($grades)) {
179
        return GRADE_UPDATE_OK;
180
    }
181
 
182
/// Finally start processing of grades
183
    if (is_object($grades)) {
184
        $grades = array($grades->userid=>$grades);
185
    } else {
186
        if (array_key_exists('userid', $grades)) {
187
            $grades = array($grades['userid']=>$grades);
188
        }
189
    }
190
 
191
/// normalize and verify grade array
192
    foreach($grades as $k=>$g) {
193
        if (!is_array($g)) {
194
            $g = (array)$g;
195
            $grades[$k] = $g;
196
        }
197
 
198
        if (empty($g['userid']) or $k != $g['userid']) {
199
            debugging('Incorrect grade array index, must be user id! Grade ignored.');
200
            unset($grades[$k]);
201
        }
202
    }
203
 
204
    if (empty($grades)) {
205
        return GRADE_UPDATE_FAILED;
206
    }
207
 
208
    $count = count($grades);
209
    if ($count > 0 and $count < 200) {
210
        list($uids, $params) = $DB->get_in_or_equal(array_keys($grades), SQL_PARAMS_NAMED, $start='uid');
211
        $params['gid'] = $gradeitem->id;
212
        $sql = "SELECT * FROM {grade_grades} WHERE itemid = :gid AND userid $uids";
213
 
214
    } else {
215
        $sql = "SELECT * FROM {grade_grades} WHERE itemid = :gid";
216
        $params = array('gid' => $gradeitem->id);
217
    }
218
 
219
    $rs = $DB->get_recordset_sql($sql, $params);
220
 
221
    $failed = false;
222
 
223
    while (count($grades) > 0) {
224
        $gradegrade = null;
225
        $grade       = null;
226
 
227
        foreach ($rs as $gd) {
228
 
229
            $userid = $gd->userid;
230
            if (!isset($grades[$userid])) {
231
                // this grade not requested, continue
232
                continue;
233
            }
234
            // existing grade requested
235
            $grade       = $grades[$userid];
236
            $gradegrade = new grade_grade($gd, false);
237
            unset($grades[$userid]);
238
            break;
239
        }
240
 
241
        if (is_null($gradegrade)) {
242
            if (count($grades) == 0) {
243
                // No more grades to process.
244
                break;
245
            }
246
 
247
            $grade       = reset($grades);
248
            $userid      = $grade['userid'];
249
            $gradegrade = new grade_grade(array('itemid' => $gradeitem->id, 'userid' => $userid), false);
250
            $gradegrade->load_optional_fields(); // add feedback and info too
251
            unset($grades[$userid]);
252
        }
253
 
254
        $rawgrade       = false;
255
        $feedback       = false;
256
        $feedbackformat = FORMAT_MOODLE;
257
        $feedbackfiles = [];
258
        $usermodified   = $USER->id;
259
        $datesubmitted  = null;
260
        $dategraded     = null;
261
 
262
        if (array_key_exists('rawgrade', $grade)) {
263
            $rawgrade = $grade['rawgrade'];
264
        }
265
 
266
        if (array_key_exists('feedback', $grade)) {
267
            $feedback = $grade['feedback'];
268
        }
269
 
270
        if (array_key_exists('feedbackformat', $grade)) {
271
            $feedbackformat = $grade['feedbackformat'];
272
        }
273
 
274
        if (array_key_exists('feedbackfiles', $grade)) {
275
            $feedbackfiles = $grade['feedbackfiles'];
276
        }
277
 
278
        if (array_key_exists('usermodified', $grade)) {
279
            $usermodified = $grade['usermodified'];
280
        }
281
 
282
        if (array_key_exists('datesubmitted', $grade)) {
283
            $datesubmitted = $grade['datesubmitted'];
284
        }
285
 
286
        if (array_key_exists('dategraded', $grade)) {
287
            $dategraded = $grade['dategraded'];
288
        }
289
 
290
        // update or insert the grade
291
        if (!$gradeitem->update_raw_grade($userid, $rawgrade, $source, $feedback, $feedbackformat, $usermodified,
292
                $dategraded, $datesubmitted, $gradegrade, $feedbackfiles, $isbulkupdate)) {
293
            $failed = true;
294
        }
295
    }
296
 
297
    if ($rs) {
298
        $rs->close();
299
    }
300
 
301
    if (!$failed) {
302
        return GRADE_UPDATE_OK;
303
    } else {
304
        return GRADE_UPDATE_FAILED;
305
    }
306
}
307
 
308
/**
309
 * Updates a user's outcomes. Manual outcomes can not be updated.
310
 *
311
 * @category grade
312
 * @param string $source Source of the grade such as 'mod/assignment'
313
 * @param int    $courseid ID of course
314
 * @param string $itemtype Type of grade item. For example, 'mod' or 'block'
315
 * @param string $itemmodule More specific then $itemtype. For example, 'forum' or 'quiz'. May be NULL for some item types
316
 * @param int    $iteminstance Instance ID of graded item. For example the forum ID.
317
 * @param int    $userid ID of the graded user
318
 * @param array  $data Array consisting of grade item itemnumber ({@link grade_update()}) => outcomegrade
319
 * @return bool returns true if grade items were found and updated successfully
320
 */
321
function grade_update_outcomes($source, $courseid, $itemtype, $itemmodule, $iteminstance, $userid, $data) {
322
    if ($items = grade_item::fetch_all(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid))) {
323
        $result = true;
324
        foreach ($items as $item) {
325
            if (!array_key_exists($item->itemnumber, $data)) {
326
                continue;
327
            }
328
            $grade = $data[$item->itemnumber] < 1 ? null : $data[$item->itemnumber];
329
            $result = ($item->update_final_grade($userid, $grade, $source) && $result);
330
        }
331
        return $result;
332
    }
333
    return false; //grade items not found
334
}
335
 
336
/**
337
 * Return true if the course needs regrading.
338
 *
339
 * @param int $courseid The course ID
340
 * @return bool true if course grades need updating.
341
 */
342
function grade_needs_regrade_final_grades($courseid) {
343
    $course_item = grade_item::fetch_course_item($courseid);
344
    return $course_item->needsupdate;
345
}
346
 
347
/**
348
 * Return true if the regrade process is likely to be time consuming and
349
 * will therefore require the progress bar.
350
 *
351
 * @param int $courseid The course ID
352
 * @return bool Whether the regrade process is likely to be time consuming
353
 */
354
function grade_needs_regrade_progress_bar($courseid) {
355
    global $DB;
356
    $grade_items = grade_item::fetch_all(array('courseid' => $courseid));
357
    if (!$grade_items) {
358
        // If there are no grade items then we definitely don't need a progress bar!
359
        return false;
360
    }
361
 
362
    list($sql, $params) = $DB->get_in_or_equal(array_keys($grade_items), SQL_PARAMS_NAMED, 'gi');
363
    $gradecount = $DB->count_records_select('grade_grades', 'itemid ' . $sql, $params);
364
 
365
    // This figure may seem arbitrary, but after analysis it seems that 100 grade_grades can be calculated in ~= 0.5 seconds.
366
    // Any longer than this and we want to show the progress bar.
367
    return $gradecount > 100;
368
}
369
 
370
/**
371
 * Check whether regarding of final grades is required and, if so, perform the regrade.
372
 *
373
 * If the regrade is expected to be time consuming (see grade_needs_regrade_progress_bar), then this
374
 * function will output the progress bar, and redirect to the current PAGE->url after regrading
375
 * completes. Otherwise the regrading will happen immediately and the page will be loaded as per
376
 * normal.
377
 *
378
 * A callback may be specified, which is called if regrading has taken place.
379
 * The callback may optionally return a URL which will be redirected to when the progress bar is present.
380
 *
381
 * @param stdClass $course The course to regrade
382
 * @param callable $callback A function to call if regrading took place
383
 * @return moodle_url|false The URL to redirect to if redirecting
384
 */
385
function grade_regrade_final_grades_if_required($course, callable $callback = null) {
386
    global $PAGE, $OUTPUT;
387
 
388
    if (!grade_needs_regrade_final_grades($course->id)) {
389
        return false;
390
    }
391
 
392
    if (grade_needs_regrade_progress_bar($course->id)) {
393
        if ($PAGE->state !== moodle_page::STATE_IN_BODY) {
394
            $PAGE->set_heading($course->fullname);
395
            echo $OUTPUT->header();
396
        }
397
        echo $OUTPUT->heading(get_string('recalculatinggrades', 'grades'));
398
        $progress = new \core\progress\display(true);
399
        $status = grade_regrade_final_grades($course->id, null, null, $progress);
400
 
401
        // Show regrade errors and set the course to no longer needing regrade (stop endless loop).
402
        if (is_array($status)) {
403
            foreach ($status as $error) {
404
                $errortext = new \core\output\notification($error, \core\output\notification::NOTIFY_ERROR);
405
                echo $OUTPUT->render($errortext);
406
            }
407
            $courseitem = grade_item::fetch_course_item($course->id);
408
            $courseitem->regrading_finished();
409
        }
410
 
411
        if ($callback) {
412
            //
413
            $url = call_user_func($callback);
414
        }
415
 
416
        if (empty($url)) {
417
            $url = $PAGE->url;
418
        }
419
 
420
        echo $OUTPUT->continue_button($url);
421
        echo $OUTPUT->footer();
422
        die();
423
    } else {
424
        $result = grade_regrade_final_grades($course->id);
425
        if ($callback) {
426
            call_user_func($callback);
427
        }
428
        return $result;
429
    }
430
}
431
 
432
/**
433
 * Returns grading information for given activity, optionally with user grades.
434
 * Manual, course or category items can not be queried.
435
 *
436
 * This function can be VERY costly - it is doing full course grades recalculation if needsupdate = 1
437
 * for course grade item. So be sure you really need it.
438
 * If you need just certain grades consider using grade_item::refresh_grades()
439
 * together with grade_item::get_grade() instead.
440
 *
441
 * @param int    $courseid ID of course
442
 * @param string $itemtype Type of grade item. For example, 'mod' or 'block'
443
 * @param string $itemmodule More specific then $itemtype. For example, 'forum' or 'quiz'. May be NULL for some item types
444
 * @param int    $iteminstance ID of the item module
445
 * @param mixed  $userid_or_ids Either a single user ID, an array of user IDs or null. If user ID or IDs are not supplied returns information about grade_item
446
 * @return stdClass Object with keys {items, outcomes, errors}, where 'items' is an array of grade
447
 *               information objects (scaleid, name, grade and locked status, etc.) indexed with itemnumbers
448
 * @category grade
449
 */
450
function grade_get_grades($courseid, $itemtype, $itemmodule, $iteminstance, $userid_or_ids=null) {
451
    global $CFG;
452
 
453
    $return = new stdClass();
454
    $return->items    = array();
455
    $return->outcomes = array();
456
    $return->errors = [];
457
 
458
    $courseitem = grade_item::fetch_course_item($courseid);
459
    $needsupdate = array();
460
    if ($courseitem->needsupdate) {
461
        $result = grade_regrade_final_grades($courseid);
462
        if ($result !== true) {
463
            $needsupdate = array_keys($result);
464
            // Return regrade errors if the user has capability.
465
            $context = context_course::instance($courseid);
466
            if (has_capability('moodle/grade:edit', $context)) {
467
                $return->errors = $result;
468
            }
469
            $courseitem->regrading_finished();
470
        }
471
    }
472
 
473
    if ($grade_items = grade_item::fetch_all(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid))) {
474
        foreach ($grade_items as $grade_item) {
475
            $decimalpoints = null;
476
 
477
            if (empty($grade_item->outcomeid)) {
478
                // prepare information about grade item
479
                $item = new stdClass();
480
                $item->id = $grade_item->id;
481
                $item->itemnumber = $grade_item->itemnumber;
482
                $item->itemtype  = $grade_item->itemtype;
483
                $item->itemmodule = $grade_item->itemmodule;
484
                $item->iteminstance = $grade_item->iteminstance;
485
                $item->scaleid    = $grade_item->scaleid;
486
                $item->name       = $grade_item->get_name();
487
                $item->grademin   = $grade_item->grademin;
488
                $item->grademax   = $grade_item->grademax;
489
                $item->gradepass  = $grade_item->gradepass;
490
                $item->locked     = $grade_item->is_locked();
491
                $item->hidden     = $grade_item->is_hidden();
492
                $item->grades     = array();
493
 
494
                switch ($grade_item->gradetype) {
495
                    case GRADE_TYPE_NONE:
496
                        break;
497
 
498
                    case GRADE_TYPE_VALUE:
499
                        $item->scaleid = 0;
500
                        break;
501
 
502
                    case GRADE_TYPE_TEXT:
503
                        $item->scaleid   = 0;
504
                        $item->grademin   = 0;
505
                        $item->grademax   = 0;
506
                        $item->gradepass  = 0;
507
                        break;
508
                }
509
 
510
                if (empty($userid_or_ids)) {
511
                    $userids = array();
512
 
513
                } else if (is_array($userid_or_ids)) {
514
                    $userids = $userid_or_ids;
515
 
516
                } else {
517
                    $userids = array($userid_or_ids);
518
                }
519
 
520
                if ($userids) {
521
                    $grade_grades = grade_grade::fetch_users_grades($grade_item, $userids, true);
522
                    foreach ($userids as $userid) {
523
                        $grade_grades[$userid]->grade_item =& $grade_item;
524
 
525
                        $grade = new stdClass();
526
                        $grade->grade          = $grade_grades[$userid]->finalgrade;
527
                        $grade->locked         = $grade_grades[$userid]->is_locked();
528
                        $grade->hidden         = $grade_grades[$userid]->is_hidden();
529
                        $grade->overridden     = $grade_grades[$userid]->overridden;
530
                        $grade->feedback       = $grade_grades[$userid]->feedback;
531
                        $grade->feedbackformat = $grade_grades[$userid]->feedbackformat;
532
                        $grade->usermodified   = $grade_grades[$userid]->usermodified;
533
                        $grade->datesubmitted  = $grade_grades[$userid]->get_datesubmitted();
534
                        $grade->dategraded     = $grade_grades[$userid]->get_dategraded();
535
 
536
                        // create text representation of grade
537
                        if ($grade_item->gradetype == GRADE_TYPE_TEXT or $grade_item->gradetype == GRADE_TYPE_NONE) {
538
                            $grade->grade          = null;
539
                            $grade->str_grade      = '-';
540
                            $grade->str_long_grade = $grade->str_grade;
541
 
542
                        } else if (in_array($grade_item->id, $needsupdate)) {
543
                            $grade->grade          = false;
544
                            $grade->str_grade      = get_string('error');
545
                            $grade->str_long_grade = $grade->str_grade;
546
 
547
                        } else if (is_null($grade->grade)) {
548
                            $grade->str_grade      = '-';
549
                            $grade->str_long_grade = $grade->str_grade;
550
 
551
                        } else {
552
                            $grade->str_grade = grade_format_gradevalue($grade->grade, $grade_item);
553
                            if ($grade_item->gradetype == GRADE_TYPE_SCALE or $grade_item->get_displaytype() != GRADE_DISPLAY_TYPE_REAL) {
554
                                $grade->str_long_grade = $grade->str_grade;
555
                            } else {
556
                                $a = new stdClass();
557
                                $a->grade = $grade->str_grade;
558
                                $a->max   = grade_format_gradevalue($grade_item->grademax, $grade_item);
559
                                $grade->str_long_grade = get_string('gradelong', 'grades', $a);
560
                            }
561
                        }
562
 
563
                        // create html representation of feedback
564
                        if (is_null($grade->feedback)) {
565
                            $grade->str_feedback = '';
566
                        } else {
567
                            $feedback = file_rewrite_pluginfile_urls(
568
                                $grade->feedback,
569
                                'pluginfile.php',
570
                                $grade_grades[$userid]->get_context()->id,
571
                                GRADE_FILE_COMPONENT,
572
                                GRADE_FEEDBACK_FILEAREA,
573
                                $grade_grades[$userid]->id
574
                            );
575
 
576
                            $grade->str_feedback = format_text($feedback, $grade->feedbackformat,
577
                                ['context' => $grade_grades[$userid]->get_context()]);
578
                        }
579
 
580
                        $item->grades[$userid] = $grade;
581
                    }
582
                }
583
                $return->items[$grade_item->itemnumber] = $item;
584
 
585
            } else {
586
                if (!$grade_outcome = grade_outcome::fetch(array('id'=>$grade_item->outcomeid))) {
587
                    debugging('Incorect outcomeid found');
588
                    continue;
589
                }
590
 
591
                // outcome info
592
                $outcome = new stdClass();
593
                $outcome->id = $grade_item->id;
594
                $outcome->itemnumber = $grade_item->itemnumber;
595
                $outcome->itemtype   = $grade_item->itemtype;
596
                $outcome->itemmodule = $grade_item->itemmodule;
597
                $outcome->iteminstance = $grade_item->iteminstance;
598
                $outcome->scaleid    = $grade_outcome->scaleid;
599
                $outcome->name       = $grade_outcome->get_name();
600
                $outcome->locked     = $grade_item->is_locked();
601
                $outcome->hidden     = $grade_item->is_hidden();
602
 
603
                if (empty($userid_or_ids)) {
604
                    $userids = array();
605
                } else if (is_array($userid_or_ids)) {
606
                    $userids = $userid_or_ids;
607
                } else {
608
                    $userids = array($userid_or_ids);
609
                }
610
 
611
                if ($userids) {
612
                    $grade_grades = grade_grade::fetch_users_grades($grade_item, $userids, true);
613
                    foreach ($userids as $userid) {
614
                        $grade_grades[$userid]->grade_item =& $grade_item;
615
 
616
                        $grade = new stdClass();
617
                        $grade->grade          = $grade_grades[$userid]->finalgrade;
618
                        $grade->locked         = $grade_grades[$userid]->is_locked();
619
                        $grade->hidden         = $grade_grades[$userid]->is_hidden();
620
                        $grade->feedback       = $grade_grades[$userid]->feedback;
621
                        $grade->feedbackformat = $grade_grades[$userid]->feedbackformat;
622
                        $grade->usermodified   = $grade_grades[$userid]->usermodified;
623
                        $grade->datesubmitted  = $grade_grades[$userid]->get_datesubmitted();
624
                        $grade->dategraded     = $grade_grades[$userid]->get_dategraded();
625
 
626
                        // create text representation of grade
627
                        if (in_array($grade_item->id, $needsupdate)) {
628
                            $grade->grade     = false;
629
                            $grade->str_grade = get_string('error');
630
 
631
                        } else if (is_null($grade->grade)) {
632
                            $grade->grade = 0;
633
                            $grade->str_grade = get_string('nooutcome', 'grades');
634
 
635
                        } else {
636
                            $grade->grade = (int)$grade->grade;
637
                            $scale = $grade_item->load_scale();
638
                            $grade->str_grade = format_string($scale->scale_items[(int)$grade->grade-1]);
639
                        }
640
 
641
                        // create html representation of feedback
642
                        if (is_null($grade->feedback)) {
643
                            $grade->str_feedback = '';
644
                        } else {
645
                            $grade->str_feedback = format_text($grade->feedback, $grade->feedbackformat);
646
                        }
647
 
648
                        $outcome->grades[$userid] = $grade;
649
                    }
650
                }
651
 
652
                if (isset($return->outcomes[$grade_item->itemnumber])) {
653
                    // itemnumber duplicates - lets fix them!
654
                    $newnumber = $grade_item->itemnumber + 1;
655
                    while(grade_item::fetch(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid, 'itemnumber'=>$newnumber))) {
656
                        $newnumber++;
657
                    }
658
                    $outcome->itemnumber    = $newnumber;
659
                    $grade_item->itemnumber = $newnumber;
660
                    $grade_item->update('system');
661
                }
662
 
663
                $return->outcomes[$grade_item->itemnumber] = $outcome;
664
 
665
            }
666
        }
667
    }
668
 
669
    // sort results using itemnumbers
670
    ksort($return->items, SORT_NUMERIC);
671
    ksort($return->outcomes, SORT_NUMERIC);
672
 
673
    return $return;
674
}
675
 
676
///////////////////////////////////////////////////////////////////
677
///// End of public API for communication with modules/blocks /////
678
///////////////////////////////////////////////////////////////////
679
 
680
 
681
 
682
///////////////////////////////////////////////////////////////////
683
///// Internal API: used by gradebook plugins and Moodle core /////
684
///////////////////////////////////////////////////////////////////
685
 
686
/**
687
 * Returns a  course gradebook setting
688
 *
689
 * @param int $courseid
690
 * @param string $name of setting, maybe null if reset only
691
 * @param string $default value to return if setting is not found
692
 * @param bool $resetcache force reset of internal static cache
693
 * @return string value of the setting, $default if setting not found, NULL if supplied $name is null
694
 */
695
function grade_get_setting($courseid, $name, $default=null, $resetcache=false) {
696
    global $DB;
697
 
698
    $cache = cache::make('core', 'gradesetting');
699
    $gradesetting = $cache->get($courseid) ?: array();
700
 
701
    if ($resetcache or empty($gradesetting)) {
702
        $gradesetting = array();
703
        $cache->set($courseid, $gradesetting);
704
 
705
    } else if (is_null($name)) {
706
        return null;
707
 
708
    } else if (array_key_exists($name, $gradesetting)) {
709
        return $gradesetting[$name];
710
    }
711
 
712
    if (!$data = $DB->get_record('grade_settings', array('courseid'=>$courseid, 'name'=>$name))) {
713
        $result = null;
714
    } else {
715
        $result = $data->value;
716
    }
717
 
718
    if (is_null($result)) {
719
        $result = $default;
720
    }
721
 
722
    $gradesetting[$name] = $result;
723
    $cache->set($courseid, $gradesetting);
724
    return $result;
725
}
726
 
727
/**
728
 * Returns all course gradebook settings as object properties
729
 *
730
 * @param int $courseid
731
 * @return object
732
 */
733
function grade_get_settings($courseid) {
734
    global $DB;
735
 
736
     $settings = new stdClass();
737
     $settings->id = $courseid;
738
 
739
    if ($records = $DB->get_records('grade_settings', array('courseid'=>$courseid))) {
740
        foreach ($records as $record) {
741
            $settings->{$record->name} = $record->value;
742
        }
743
    }
744
 
745
    return $settings;
746
}
747
 
748
/**
749
 * Add, update or delete a course gradebook setting
750
 *
751
 * @param int $courseid The course ID
752
 * @param string $name Name of the setting
753
 * @param string $value Value of the setting. NULL means delete the setting.
754
 */
755
function grade_set_setting($courseid, $name, $value) {
756
    global $DB;
757
 
758
    if (is_null($value)) {
759
        $DB->delete_records('grade_settings', array('courseid'=>$courseid, 'name'=>$name));
760
 
761
    } else if (!$existing = $DB->get_record('grade_settings', array('courseid'=>$courseid, 'name'=>$name))) {
762
        $data = new stdClass();
763
        $data->courseid = $courseid;
764
        $data->name     = $name;
765
        $data->value    = $value;
766
        $DB->insert_record('grade_settings', $data);
767
 
768
    } else {
769
        $data = new stdClass();
770
        $data->id       = $existing->id;
771
        $data->value    = $value;
772
        $DB->update_record('grade_settings', $data);
773
    }
774
 
775
    grade_get_setting($courseid, null, null, true); // reset the cache
776
}
777
 
778
/**
779
 * Returns string representation of grade value
780
 *
781
 * @param float|null $value The grade value
782
 * @param object $grade_item Grade item object passed by reference to prevent scale reloading
783
 * @param bool $localized use localised decimal separator
784
 * @param int $displaytype type of display. For example GRADE_DISPLAY_TYPE_REAL, GRADE_DISPLAY_TYPE_PERCENTAGE, GRADE_DISPLAY_TYPE_LETTER
785
 * @param int $decimals The number of decimal places when displaying float values
786
 * @return string
787
 */
788
function grade_format_gradevalue(?float $value, &$grade_item, $localized=true, $displaytype=null, $decimals=null) {
789
    if ($grade_item->gradetype == GRADE_TYPE_NONE or $grade_item->gradetype == GRADE_TYPE_TEXT) {
790
        return '';
791
    }
792
 
793
    // no grade yet?
794
    if (is_null($value)) {
795
        return '-';
796
    }
797
 
798
    if ($grade_item->gradetype != GRADE_TYPE_VALUE and $grade_item->gradetype != GRADE_TYPE_SCALE) {
799
        //unknown type??
800
        return '';
801
    }
802
 
803
    if (is_null($displaytype)) {
804
        $displaytype = $grade_item->get_displaytype();
805
    }
806
 
807
    if (is_null($decimals)) {
808
        $decimals = $grade_item->get_decimals();
809
    }
810
 
811
    switch ($displaytype) {
812
        case GRADE_DISPLAY_TYPE_REAL:
813
            return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized);
814
 
815
        case GRADE_DISPLAY_TYPE_PERCENTAGE:
816
            return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized);
817
 
818
        case GRADE_DISPLAY_TYPE_LETTER:
819
            return grade_format_gradevalue_letter($value, $grade_item);
820
 
821
        case GRADE_DISPLAY_TYPE_REAL_PERCENTAGE:
822
            return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ' (' .
823
                    grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ')';
824
 
825
        case GRADE_DISPLAY_TYPE_REAL_LETTER:
826
            return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ' (' .
827
                    grade_format_gradevalue_letter($value, $grade_item) . ')';
828
 
829
        case GRADE_DISPLAY_TYPE_PERCENTAGE_REAL:
830
            return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ' (' .
831
                    grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ')';
832
 
833
        case GRADE_DISPLAY_TYPE_LETTER_REAL:
834
            return grade_format_gradevalue_letter($value, $grade_item) . ' (' .
835
                    grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ')';
836
 
837
        case GRADE_DISPLAY_TYPE_LETTER_PERCENTAGE:
838
            return grade_format_gradevalue_letter($value, $grade_item) . ' (' .
839
                    grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ')';
840
 
841
        case GRADE_DISPLAY_TYPE_PERCENTAGE_LETTER:
842
            return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ' (' .
843
                    grade_format_gradevalue_letter($value, $grade_item) . ')';
844
        default:
845
            return '';
846
    }
847
}
848
 
849
/**
850
 * Returns a float representation of a grade value
851
 *
852
 * @param float|null $value The grade value
853
 * @param object $grade_item Grade item object
854
 * @param int $decimals The number of decimal places
855
 * @param bool $localized use localised decimal separator
856
 * @return string
857
 */
858
function grade_format_gradevalue_real(?float $value, $grade_item, $decimals, $localized) {
859
    if ($grade_item->gradetype == GRADE_TYPE_SCALE) {
860
        if (!$scale = $grade_item->load_scale()) {
861
            return get_string('error');
862
        }
863
 
864
        $value = $grade_item->bounded_grade($value);
865
        return format_string($scale->scale_items[$value-1]);
866
 
867
    } else {
868
        return format_float($value, $decimals, $localized);
869
    }
870
}
871
 
872
/**
873
 * Returns a percentage representation of a grade value
874
 *
875
 * @param float|null $value The grade value
876
 * @param object $grade_item Grade item object
877
 * @param int $decimals The number of decimal places
878
 * @param bool $localized use localised decimal separator
879
 * @return string
880
 */
881
function grade_format_gradevalue_percentage(?float $value, $grade_item, $decimals, $localized) {
882
    $min = $grade_item->grademin;
883
    $max = $grade_item->grademax;
884
    if ($min == $max) {
885
        return '';
886
    }
887
    $value = $grade_item->bounded_grade($value);
888
    $percentage = (($value-$min)*100)/($max-$min);
889
    return format_float($percentage, $decimals, $localized).' %';
890
}
891
 
892
/**
893
 * Returns a letter grade representation of a grade value
894
 * The array of grade letters used is produced by {@link grade_get_letters()} using the course context
895
 *
896
 * @param float|null $value The grade value
897
 * @param object $grade_item Grade item object
898
 * @return string
899
 */
900
function grade_format_gradevalue_letter(?float $value, $grade_item) {
901
    global $CFG;
902
    $context = context_course::instance($grade_item->courseid, IGNORE_MISSING);
903
    if (!$letters = grade_get_letters($context)) {
904
        return ''; // no letters??
905
    }
906
 
907
    if (is_null($value)) {
908
        return '-';
909
    }
910
 
911
    $value = grade_grade::standardise_score($value, $grade_item->grademin, $grade_item->grademax, 0, 100);
912
    $value = bounded_number(0, $value, 100); // just in case
913
 
914
    $gradebookcalculationsfreeze = 'gradebook_calculations_freeze_' . $grade_item->courseid;
915
 
916
    foreach ($letters as $boundary => $letter) {
917
        if (property_exists($CFG, $gradebookcalculationsfreeze) && (int)$CFG->{$gradebookcalculationsfreeze} <= 20160518) {
918
            // Do nothing.
919
        } else {
920
            // The boundary is a percentage out of 100 so use 0 as the min and 100 as the max.
921
            $boundary = grade_grade::standardise_score($boundary, 0, 100, 0, 100);
922
        }
923
        if ($value >= $boundary) {
924
            return format_string($letter);
925
        }
926
    }
927
    return '-'; // no match? maybe '' would be more correct
928
}
929
 
930
 
931
/**
932
 * Returns grade options for gradebook grade category menu
933
 *
934
 * @param int $courseid The course ID
935
 * @param bool $includenew Include option for new category at array index -1
936
 * @return array of grade categories in course
937
 */
938
function grade_get_categories_menu($courseid, $includenew=false) {
939
    $result = array();
940
    if (!$categories = grade_category::fetch_all(array('courseid'=>$courseid))) {
941
        //make sure course category exists
942
        if (!grade_category::fetch_course_category($courseid)) {
943
            debugging('Can not create course grade category!');
944
            return $result;
945
        }
946
        $categories = grade_category::fetch_all(array('courseid'=>$courseid));
947
    }
948
    foreach ($categories as $key=>$category) {
949
        if ($category->is_course_category()) {
950
            $result[$category->id] = get_string('uncategorised', 'grades');
951
            unset($categories[$key]);
952
        }
953
    }
954
    if ($includenew) {
955
        $result[-1] = get_string('newcategory', 'grades');
956
    }
957
    $cats = array();
958
    foreach ($categories as $category) {
959
        $cats[$category->id] = $category->get_name();
960
    }
961
    core_collator::asort($cats);
962
 
963
    return ($result+$cats);
964
}
965
 
966
/**
967
 * Returns the array of grade letters to be used in the supplied context
968
 *
969
 * @param object $context Context object or null for defaults
970
 * @return array of grade_boundary (minimum) => letter_string
971
 */
972
function grade_get_letters($context=null) {
973
    global $DB;
974
 
975
    if (empty($context)) {
976
        //default grading letters
977
        return array('93'=>'A', '90'=>'A-', '87'=>'B+', '83'=>'B', '80'=>'B-', '77'=>'C+', '73'=>'C', '70'=>'C-', '67'=>'D+', '60'=>'D', '0'=>'F');
978
    }
979
 
980
    $cache = cache::make('core', 'grade_letters');
981
    $data = $cache->get($context->id);
982
 
983
    if (!empty($data)) {
984
        return $data;
985
    }
986
 
987
    $letters = array();
988
 
989
    $contexts = $context->get_parent_context_ids();
990
    array_unshift($contexts, $context->id);
991
 
992
    foreach ($contexts as $ctxid) {
993
        if ($records = $DB->get_records('grade_letters', array('contextid'=>$ctxid), 'lowerboundary DESC')) {
994
            foreach ($records as $record) {
995
                $letters[$record->lowerboundary] = $record->letter;
996
            }
997
        }
998
 
999
        if (!empty($letters)) {
1000
            // Cache the grade letters for this context.
1001
            $cache->set($context->id, $letters);
1002
            return $letters;
1003
        }
1004
    }
1005
 
1006
    $letters = grade_get_letters(null);
1007
    // Cache the grade letters for this context.
1008
    $cache->set($context->id, $letters);
1009
    return $letters;
1010
}
1011
 
1012
 
1013
/**
1014
 * Verify new value of grade item idnumber. Checks for uniqueness of new ID numbers. Old ID numbers are kept intact.
1015
 *
1016
 * @param string $idnumber string (with magic quotes)
1017
 * @param int $courseid ID numbers are course unique only
1018
 * @param grade_item $grade_item The grade item this idnumber is associated with
1019
 * @param stdClass $cm used for course module idnumbers and items attached to modules
1020
 * @return bool true means idnumber ok
1021
 */
1022
function grade_verify_idnumber($idnumber, $courseid, $grade_item=null, $cm=null) {
1023
    global $DB;
1024
 
1025
    if ($idnumber == '') {
1026
        //we allow empty idnumbers
1027
        return true;
1028
    }
1029
 
1030
    // keep existing even when not unique
1031
    if ($cm and $cm->idnumber == $idnumber) {
1032
        if ($grade_item and $grade_item->itemnumber != 0) {
1033
            // grade item with itemnumber > 0 can't have the same idnumber as the main
1034
            // itemnumber 0 which is synced with course_modules
1035
            return false;
1036
        }
1037
        return true;
1038
    } else if ($grade_item and $grade_item->idnumber == $idnumber) {
1039
        return true;
1040
    }
1041
 
1042
    if ($DB->record_exists('course_modules', array('course'=>$courseid, 'idnumber'=>$idnumber))) {
1043
        return false;
1044
    }
1045
 
1046
    if ($DB->record_exists('grade_items', array('courseid'=>$courseid, 'idnumber'=>$idnumber))) {
1047
        return false;
1048
    }
1049
 
1050
    return true;
1051
}
1052
 
1053
/**
1054
 * Force final grade recalculation in all course items
1055
 *
1056
 * @param int $courseid The course ID to recalculate
1057
 */
1058
function grade_force_full_regrading($courseid) {
1059
    global $DB;
1060
    $DB->set_field('grade_items', 'needsupdate', 1, array('courseid'=>$courseid));
1061
}
1062
 
1063
/**
1064
 * Forces regrading of all site grades. Used when changing site setings
1065
 */
1066
function grade_force_site_regrading() {
1067
    global $CFG, $DB;
1068
    $DB->set_field('grade_items', 'needsupdate', 1);
1069
}
1070
 
1071
/**
1072
 * Recover a user's grades from grade_grades_history
1073
 * @param int $userid the user ID whose grades we want to recover
1074
 * @param int $courseid the relevant course
1075
 * @return bool true if successful or false if there was an error or no grades could be recovered
1076
 */
1077
function grade_recover_history_grades($userid, $courseid) {
1078
    global $CFG, $DB;
1079
 
1080
    if ($CFG->disablegradehistory) {
1081
        debugging('Attempting to recover grades when grade history is disabled.');
1082
        return false;
1083
    }
1084
 
1085
    //Were grades recovered? Flag to return.
1086
    $recoveredgrades = false;
1087
 
1088
    //Check the user is enrolled in this course
1089
    //Dont bother checking if they have a gradeable role. They may get one later so recover
1090
    //whatever grades they have now just in case.
1091
    $course_context = context_course::instance($courseid);
1092
    if (!is_enrolled($course_context, $userid)) {
1093
        debugging('Attempting to recover the grades of a user who is deleted or not enrolled. Skipping recover.');
1094
        return false;
1095
    }
1096
 
1097
    //Check for existing grades for this user in this course
1098
    //Recovering grades when the user already has grades can lead to duplicate indexes and bad data
1099
    //In the future we could move the existing grades to the history table then recover the grades from before then
1100
    $sql = "SELECT gg.id
1101
              FROM {grade_grades} gg
1102
              JOIN {grade_items} gi ON gi.id = gg.itemid
1103
             WHERE gi.courseid = :courseid AND gg.userid = :userid";
1104
    $params = array('userid' => $userid, 'courseid' => $courseid);
1105
    if ($DB->record_exists_sql($sql, $params)) {
1106
        debugging('Attempting to recover the grades of a user who already has grades. Skipping recover.');
1107
        return false;
1108
    } else {
1109
        //Retrieve the user's old grades
1110
        //have history ID as first column to guarantee we a unique first column
1111
        $sql = "SELECT h.id, gi.itemtype, gi.itemmodule, gi.iteminstance as iteminstance, gi.itemnumber, h.source, h.itemid, h.userid, h.rawgrade, h.rawgrademax,
1112
                       h.rawgrademin, h.rawscaleid, h.usermodified, h.finalgrade, h.hidden, h.locked, h.locktime, h.exported, h.overridden, h.excluded, h.feedback,
1113
                       h.feedbackformat, h.information, h.informationformat, h.timemodified, itemcreated.tm AS timecreated
1114
                  FROM {grade_grades_history} h
1115
                  JOIN (SELECT itemid, MAX(id) AS id
1116
                          FROM {grade_grades_history}
1117
                         WHERE userid = :userid1
1118
                      GROUP BY itemid) maxquery ON h.id = maxquery.id AND h.itemid = maxquery.itemid
1119
                  JOIN {grade_items} gi ON gi.id = h.itemid
1120
                  JOIN (SELECT itemid, MAX(timemodified) AS tm
1121
                          FROM {grade_grades_history}
1122
                         WHERE userid = :userid2 AND action = :insertaction
1123
                      GROUP BY itemid) itemcreated ON itemcreated.itemid = h.itemid
1124
                 WHERE gi.courseid = :courseid";
1125
        $params = array('userid1' => $userid, 'userid2' => $userid , 'insertaction' => GRADE_HISTORY_INSERT, 'courseid' => $courseid);
1126
        $oldgrades = $DB->get_records_sql($sql, $params);
1127
 
1128
        //now move the old grades to the grade_grades table
1129
        foreach ($oldgrades as $oldgrade) {
1130
            unset($oldgrade->id);
1131
 
1132
            $grade = new grade_grade($oldgrade, false);//2nd arg false as dont want to try and retrieve a record from the DB
1133
            $grade->insert($oldgrade->source);
1134
 
1135
            //dont include default empty grades created when activities are created
1136
            if (!is_null($oldgrade->finalgrade) || !is_null($oldgrade->feedback)) {
1137
                $recoveredgrades = true;
1138
            }
1139
        }
1140
    }
1141
 
1142
    //Some activities require manual grade synching (moving grades from the activity into the gradebook)
1143
    //If the student was deleted when synching was done they may have grades in the activity that haven't been moved across
1144
    grade_grab_course_grades($courseid, null, $userid);
1145
 
1146
    return $recoveredgrades;
1147
}
1148
 
1149
/**
1150
 * Updates all final grades in course.
1151
 *
1152
 * @param int $courseid The course ID
1153
 * @param int $userid If specified try to do a quick regrading of the grades of this user only
1154
 * @param object $updated_item Optional grade item to be marked for regrading. It is required if $userid is set.
1155
 * @param \core\progress\base $progress If provided, will be used to update progress on this long operation.
1156
 * @return array|true true if ok, array of errors if problems found. Grade item id => error message
1157
 */
1158
function grade_regrade_final_grades($courseid, $userid=null, $updated_item=null, $progress=null) {
1159
    // This may take a very long time and extra memory.
1160
    \core_php_time_limit::raise();
1161
    raise_memory_limit(MEMORY_EXTRA);
1162
 
1163
    $course_item = grade_item::fetch_course_item($courseid);
1164
 
1165
    if ($progress == null) {
1166
        $progress = new \core\progress\none();
1167
    }
1168
 
1169
    if ($userid) {
1170
        // one raw grade updated for one user
1171
        if (empty($updated_item)) {
1172
            throw new \moodle_exception("cannotbenull", 'debug', '', "updated_item");
1173
        }
1174
        if ($course_item->needsupdate) {
1175
            $updated_item->force_regrading();
1176
            return array($course_item->id =>'Can not do fast regrading after updating of raw grades');
1177
        }
1178
 
1179
    } else {
1180
        if (!$course_item->needsupdate) {
1181
            // nothing to do :-)
1182
            return true;
1183
        }
1184
    }
1185
 
1186
    // Categories might have to run some processing before we fetch the grade items.
1187
    // This gives them a final opportunity to update and mark their children to be updated.
1188
    // We need to work on the children categories up to the parent ones, so that, for instance,
1189
    // if a category total is updated it will be reflected in the parent category.
1190
    $cats = grade_category::fetch_all(array('courseid' => $courseid));
1191
    $flatcattree = array();
1192
    foreach ($cats as $cat) {
1193
        if (!isset($flatcattree[$cat->depth])) {
1194
            $flatcattree[$cat->depth] = array();
1195
        }
1196
        $flatcattree[$cat->depth][] = $cat;
1197
    }
1198
    krsort($flatcattree);
1199
    foreach ($flatcattree as $depth => $cats) {
1200
        foreach ($cats as $cat) {
1201
            $cat->pre_regrade_final_grades();
1202
        }
1203
    }
1204
 
1205
    $progresstotal = 0;
1206
    $progresscurrent = 0;
1207
 
1208
    $grade_items = grade_item::fetch_all(array('courseid'=>$courseid));
1209
    $depends_on = array();
1210
 
1211
    foreach ($grade_items as $gid=>$gitem) {
1212
        if ((!empty($updated_item) and $updated_item->id == $gid) ||
1213
                $gitem->is_course_item() || $gitem->is_category_item() || $gitem->is_calculated()) {
1214
            $grade_items[$gid]->needsupdate = 1;
1215
        }
1216
 
1217
        // We load all dependencies of these items later we can discard some grade_items based on this.
1218
        if ($grade_items[$gid]->needsupdate) {
1219
            $depends_on[$gid] = $grade_items[$gid]->depends_on();
1220
            $progresstotal++;
1221
        }
1222
    }
1223
 
1224
    $progress->start_progress('regrade_course', $progresstotal);
1225
 
1226
    $errors = array();
1227
    $finalids = array();
1228
    $updatedids = array();
1229
    $gids     = array_keys($grade_items);
1230
    $failed = 0;
1231
 
1232
    while (count($finalids) < count($gids)) { // work until all grades are final or error found
1233
        $count = 0;
1234
        foreach ($gids as $gid) {
1235
            if (in_array($gid, $finalids)) {
1236
                continue; // already final
1237
            }
1238
 
1239
            if (!$grade_items[$gid]->needsupdate) {
1240
                $finalids[] = $gid; // we can make it final - does not need update
1241
                continue;
1242
            }
1243
            $thisprogress = $progresstotal;
1244
            foreach ($grade_items as $item) {
1245
                if ($item->needsupdate) {
1246
                    $thisprogress--;
1247
                }
1248
            }
1249
            // Clip between $progresscurrent and $progresstotal.
1250
            $thisprogress = max(min($thisprogress, $progresstotal), $progresscurrent);
1251
            $progress->progress($thisprogress);
1252
            $progresscurrent = $thisprogress;
1253
 
1254
            foreach ($depends_on[$gid] as $did) {
1255
                if (!in_array($did, $finalids)) {
1256
                    // This item depends on something that is not yet in finals array.
1257
                    continue 2;
1258
                }
1259
            }
1260
 
1261
            // If this grade item has no dependancy with any updated item at all, then remove it from being recalculated.
1262
 
1263
            // When we get here, all of this grade item's decendents are marked as final so they would be marked as updated too
1264
            // if they would have been regraded. We don't need to regrade items which dependants (not only the direct ones
1265
            // but any dependant in the cascade) have not been updated.
1266
 
1267
            // If $updated_item was specified we discard the grade items that do not depend on it or on any grade item that
1268
            // depend on $updated_item.
1269
 
1270
            // Here we check to see if the direct decendants are marked as updated.
1271
            if (!empty($updated_item) && $gid != $updated_item->id && !in_array($updated_item->id, $depends_on[$gid])) {
1272
 
1273
                // We need to ensure that none of this item's dependencies have been updated.
1274
                // If we find that one of the direct decendants of this grade item is marked as updated then this
1275
                // grade item needs to be recalculated and marked as updated.
1276
                // Being marked as updated is done further down in the code.
1277
 
1278
                $updateddependencies = false;
1279
                foreach ($depends_on[$gid] as $dependency) {
1280
                    if (in_array($dependency, $updatedids)) {
1281
                        $updateddependencies = true;
1282
                        break;
1283
                    }
1284
                }
1285
                if ($updateddependencies === false) {
1286
                    // If no direct descendants are marked as updated, then we don't need to update this grade item. We then mark it
1287
                    // as final.
1288
                    $count++;
1289
                    $finalids[] = $gid;
1290
                    continue;
1291
                }
1292
            }
1293
 
1294
            // Let's update, calculate or aggregate.
1295
            $result = $grade_items[$gid]->regrade_final_grades($userid, $progress);
1296
 
1297
            if ($result === true) {
1298
 
1299
                // We should only update the database if we regraded all users.
1300
                if (empty($userid)) {
1301
                    $grade_items[$gid]->regrading_finished();
1302
                    // Do the locktime item locking.
1303
                    $grade_items[$gid]->check_locktime();
1304
                } else {
1305
                    $grade_items[$gid]->needsupdate = 0;
1306
                }
1307
                $count++;
1308
                $finalids[] = $gid;
1309
                $updatedids[] = $gid;
1310
 
1311
            } else {
1312
                $grade_items[$gid]->force_regrading();
1313
                $errors[$gid] = $result;
1314
            }
1315
        }
1316
 
1317
        if ($count == 0) {
1318
            $failed++;
1319
        } else {
1320
            $failed = 0;
1321
        }
1322
 
1323
        if ($failed > 1) {
1324
            foreach($gids as $gid) {
1325
                if (in_array($gid, $finalids)) {
1326
                    continue; // this one is ok
1327
                }
1328
                $grade_items[$gid]->force_regrading();
1329
                if (!empty($grade_items[$gid]->calculation) && empty($errors[$gid])) {
1330
                    $itemname = $grade_items[$gid]->get_name();
1331
                    $errors[$gid] = get_string('errorcalculationbroken', 'grades', $itemname);
1332
                }
1333
            }
1334
            break; // Found error.
1335
        }
1336
    }
1337
    $progress->end_progress();
1338
 
1339
    if (count($errors) == 0) {
1340
        if (empty($userid)) {
1341
            // do the locktime locking of grades, but only when doing full regrading
1342
            grade_grade::check_locktime_all($gids);
1343
        }
1344
        return true;
1345
    } else {
1346
        return $errors;
1347
    }
1348
}
1349
 
1350
/**
1351
 * Refetches grade data from course activities
1352
 *
1353
 * @param int $courseid The course ID
1354
 * @param string $modname Limit the grade fetch to a single module type. For example 'forum'
1355
 * @param int $userid limit the grade fetch to a single user
1356
 */
1357
function grade_grab_course_grades($courseid, $modname=null, $userid=0) {
1358
    global $CFG, $DB;
1359
 
1360
    if ($modname) {
1361
        $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname
1362
                  FROM {".$modname."} a, {course_modules} cm, {modules} m
1363
                 WHERE m.name=:modname AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=:courseid";
1364
        $params = array('modname'=>$modname, 'courseid'=>$courseid);
1365
 
1366
        if ($modinstances = $DB->get_records_sql($sql, $params)) {
1367
            foreach ($modinstances as $modinstance) {
1368
                grade_update_mod_grades($modinstance, $userid);
1369
            }
1370
        }
1371
        return;
1372
    }
1373
 
1374
    if (!$mods = core_component::get_plugin_list('mod') ) {
1375
        throw new \moodle_exception('nomodules', 'debug');
1376
    }
1377
 
1378
    foreach ($mods as $mod => $fullmod) {
1379
        if ($mod == 'NEWMODULE') {   // Someone has unzipped the template, ignore it
1380
            continue;
1381
        }
1382
 
1383
        // include the module lib once
1384
        if (file_exists($fullmod.'/lib.php')) {
1385
            // get all instance of the activity
1386
            $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname
1387
                      FROM {".$mod."} a, {course_modules} cm, {modules} m
1388
                     WHERE m.name=:mod AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=:courseid";
1389
            $params = array('mod'=>$mod, 'courseid'=>$courseid);
1390
 
1391
            if ($modinstances = $DB->get_records_sql($sql, $params)) {
1392
                foreach ($modinstances as $modinstance) {
1393
                    grade_update_mod_grades($modinstance, $userid);
1394
                }
1395
            }
1396
        }
1397
    }
1398
}
1399
 
1400
/**
1401
 * Force full update of module grades in central gradebook
1402
 *
1403
 * @param object $modinstance Module object with extra cmidnumber and modname property
1404
 * @param int $userid Optional user ID if limiting the update to a single user
1405
 * @return bool True if success
1406
 */
1407
function grade_update_mod_grades($modinstance, $userid=0) {
1408
    global $CFG, $DB;
1409
 
1410
    $fullmod = $CFG->dirroot.'/mod/'.$modinstance->modname;
1411
    if (!file_exists($fullmod.'/lib.php')) {
1412
        debugging('missing lib.php file in module ' . $modinstance->modname);
1413
        return false;
1414
    }
1415
    include_once($fullmod.'/lib.php');
1416
 
1417
    $updateitemfunc   = $modinstance->modname.'_grade_item_update';
1418
    $updategradesfunc = $modinstance->modname.'_update_grades';
1419
 
1420
    if (function_exists($updategradesfunc) and function_exists($updateitemfunc)) {
1421
        //new grading supported, force updating of grades
1422
        $updateitemfunc($modinstance);
1423
        $updategradesfunc($modinstance, $userid);
1424
    } else if (function_exists($updategradesfunc) xor function_exists($updateitemfunc)) {
1425
        // Module does not support grading?
1426
        debugging("You have declared one of $updateitemfunc and $updategradesfunc but not both. " .
1427
                  "This will cause broken behaviour.", DEBUG_DEVELOPER);
1428
    }
1429
 
1430
    return true;
1431
}
1432
 
1433
/**
1434
 * Remove grade letters for given context
1435
 *
1436
 * @param context $context The context
1437
 * @param bool $showfeedback If true a success notification will be displayed
1438
 */
1439
function remove_grade_letters($context, $showfeedback) {
1440
    global $DB, $OUTPUT;
1441
 
1442
    $strdeleted = get_string('deleted');
1443
 
1444
    $records = $DB->get_records('grade_letters', array('contextid' => $context->id));
1445
    foreach ($records as $record) {
1446
        $DB->delete_records('grade_letters', array('id' => $record->id));
1447
        // Trigger the letter grade deleted event.
1448
        $event = \core\event\grade_letter_deleted::create(array(
1449
            'objectid' => $record->id,
1450
            'context' => $context,
1451
        ));
1452
        $event->trigger();
1453
    }
1454
    if ($showfeedback) {
1455
        echo $OUTPUT->notification($strdeleted.' - '.get_string('letters', 'grades'), 'notifysuccess');
1456
    }
1457
 
1458
    $cache = cache::make('core', 'grade_letters');
1459
    $cache->delete($context->id);
1460
}
1461
 
1462
/**
1463
 * Remove all grade related course data
1464
 * Grade history is kept
1465
 *
1466
 * @param int $courseid The course ID
1467
 * @param bool $showfeedback If true success notifications will be displayed
1468
 */
1469
function remove_course_grades($courseid, $showfeedback) {
1470
    global $DB, $OUTPUT;
1471
 
1472
    $fs = get_file_storage();
1473
    $strdeleted = get_string('deleted');
1474
 
1475
    $course_category = grade_category::fetch_course_category($courseid);
1476
    $course_category->delete('coursedelete');
1477
    $fs->delete_area_files(context_course::instance($courseid)->id, 'grade', 'feedback');
1478
    if ($showfeedback) {
1479
        echo $OUTPUT->notification($strdeleted.' - '.get_string('grades', 'grades').', '.get_string('items', 'grades').', '.get_string('categories', 'grades'), 'notifysuccess');
1480
    }
1481
 
1482
    if ($outcomes = grade_outcome::fetch_all(array('courseid'=>$courseid))) {
1483
        foreach ($outcomes as $outcome) {
1484
            $outcome->delete('coursedelete');
1485
        }
1486
    }
1487
    $DB->delete_records('grade_outcomes_courses', array('courseid'=>$courseid));
1488
    if ($showfeedback) {
1489
        echo $OUTPUT->notification($strdeleted.' - '.get_string('outcomes', 'grades'), 'notifysuccess');
1490
    }
1491
 
1492
    if ($scales = grade_scale::fetch_all(array('courseid'=>$courseid))) {
1493
        foreach ($scales as $scale) {
1494
            $scale->delete('coursedelete');
1495
        }
1496
    }
1497
    if ($showfeedback) {
1498
        echo $OUTPUT->notification($strdeleted.' - '.get_string('scales'), 'notifysuccess');
1499
    }
1500
 
1501
    $DB->delete_records('grade_settings', array('courseid'=>$courseid));
1502
    if ($showfeedback) {
1503
        echo $OUTPUT->notification($strdeleted.' - '.get_string('settings', 'grades'), 'notifysuccess');
1504
    }
1505
}
1506
 
1507
/**
1508
 * Called when course category is deleted
1509
 * Cleans the gradebook of associated data
1510
 *
1511
 * @param int $categoryid The course category id
1512
 * @param int $newparentid If empty everything is deleted. Otherwise the ID of the category where content moved
1513
 * @param bool $showfeedback print feedback
1514
 */
1515
function grade_course_category_delete($categoryid, $newparentid, $showfeedback) {
1516
    global $DB;
1517
 
1518
    $context = context_coursecat::instance($categoryid);
1519
    $records = $DB->get_records('grade_letters', array('contextid' => $context->id));
1520
    foreach ($records as $record) {
1521
        $DB->delete_records('grade_letters', array('id' => $record->id));
1522
        // Trigger the letter grade deleted event.
1523
        $event = \core\event\grade_letter_deleted::create(array(
1524
            'objectid' => $record->id,
1525
            'context' => $context,
1526
        ));
1527
        $event->trigger();
1528
    }
1529
}
1530
 
1531
/**
1532
 * Does gradebook cleanup when a module is uninstalled
1533
 * Deletes all associated grade items
1534
 *
1535
 * @param string $modname The grade item module name to remove. For example 'forum'
1536
 */
1537
function grade_uninstalled_module($modname) {
1538
    global $CFG, $DB;
1539
 
1540
    $sql = "SELECT *
1541
              FROM {grade_items}
1542
             WHERE itemtype='mod' AND itemmodule=?";
1543
 
1544
    // go all items for this module and delete them including the grades
1545
    $rs = $DB->get_recordset_sql($sql, array($modname));
1546
    foreach ($rs as $item) {
1547
        $grade_item = new grade_item($item, false);
1548
        $grade_item->delete('moduninstall');
1549
    }
1550
    $rs->close();
1551
}
1552
 
1553
/**
1554
 * Deletes all of a user's grade data from gradebook
1555
 *
1556
 * @param int $userid The user whose grade data should be deleted
1557
 */
1558
function grade_user_delete($userid) {
1559
    if ($grades = grade_grade::fetch_all(array('userid'=>$userid))) {
1560
        foreach ($grades as $grade) {
1561
            $grade->delete('userdelete');
1562
        }
1563
    }
1564
}
1565
 
1566
/**
1567
 * Purge course data when user unenrolls from a course
1568
 *
1569
 * @param int $courseid The ID of the course the user has unenrolled from
1570
 * @param int $userid The ID of the user unenrolling
1571
 */
1572
function grade_user_unenrol($courseid, $userid) {
1573
    if ($items = grade_item::fetch_all(array('courseid'=>$courseid))) {
1574
        foreach ($items as $item) {
1575
            if ($grades = grade_grade::fetch_all(array('userid'=>$userid, 'itemid'=>$item->id))) {
1576
                foreach ($grades as $grade) {
1577
                    $grade->delete('userdelete');
1578
                }
1579
            }
1580
        }
1581
    }
1582
}
1583
 
1584
/**
1585
 * Reset all course grades, refetch from the activities and recalculate
1586
 *
1587
 * @param int $courseid The course to reset
1588
 * @return bool success
1589
 */
1590
function grade_course_reset($courseid) {
1591
 
1592
    // no recalculations
1593
    grade_force_full_regrading($courseid);
1594
 
1595
    $grade_items = grade_item::fetch_all(array('courseid'=>$courseid));
1596
    foreach ($grade_items as $gid=>$grade_item) {
1597
        $grade_item->delete_all_grades('reset');
1598
    }
1599
 
1600
    //refetch all grades
1601
    grade_grab_course_grades($courseid);
1602
 
1603
    // recalculate all grades
1604
    grade_regrade_final_grades($courseid);
1605
    return true;
1606
}
1607
 
1608
/**
1609
 * Convert a number to 5 decimal point float, null db compatible format
1610
 * (we need this to decide if db value changed)
1611
 *
1612
 * @param float|null $number The number to convert
1613
 * @return float|null float or null
1614
 */
1615
function grade_floatval(?float $number) {
1616
    if (is_null($number)) {
1617
        return null;
1618
    }
1619
    // we must round to 5 digits to get the same precision as in 10,5 db fields
1620
    // note: db rounding for 10,5 is different from php round() function
1621
    return round($number, 5);
1622
}
1623
 
1624
/**
1625
 * Compare two float numbers safely. Uses 5 decimals php precision using {@link grade_floatval()}. Nulls accepted too.
1626
 * Used for determining if a database update is required
1627
 *
1628
 * @param float|null $f1 Float one to compare
1629
 * @param float|null $f2 Float two to compare
1630
 * @return bool True if the supplied values are different
1631
 */
1632
function grade_floats_different(?float $f1, ?float $f2): bool {
1633
    // note: db rounding for 10,5 is different from php round() function
1634
    return (grade_floatval($f1) !== grade_floatval($f2));
1635
}
1636
 
1637
/**
1638
 * Compare two float numbers safely. Uses 5 decimals php precision using {@link grade_floatval()}
1639
 *
1640
 * Do not use rounding for 10,5 at the database level as the results may be
1641
 * different from php round() function.
1642
 *
1643
 * @since Moodle 2.0
1644
 * @param float|null $f1 Float one to compare
1645
 * @param float|null $f2 Float two to compare
1646
 * @return bool True if the values should be considered as the same grades
1647
 */
1648
function grade_floats_equal(?float $f1, ?float $f2): bool {
1649
    return (grade_floatval($f1) === grade_floatval($f2));
1650
}
1651
 
1652
/**
1653
 * Get the most appropriate grade date for a grade item given the user that the grade relates to.
1654
 *
1655
 * @param \stdClass $grade
1656
 * @param \stdClass $user
1657
 * @return int|null
1658
 */
1659
function grade_get_date_for_user_grade(\stdClass $grade, \stdClass $user): ?int {
1660
    // The `datesubmitted` is the time that the grade was created.
1661
    // The `dategraded` is the time that it was modified or overwritten.
1662
    // If the grade was last modified by the user themselves use the date graded.
1663
    // Otherwise use date submitted.
1664
    if ($grade->usermodified == $user->id || empty($grade->datesubmitted)) {
1665
        return $grade->dategraded;
1666
    } else {
1667
        return $grade->datesubmitted;
1668
    }
1669
}