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
 * 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
 */
1441 ariadna 385
function grade_regrade_final_grades_if_required($course, ?callable $callback = null) {
386
    global $PAGE;
1 efrain 387
 
388
    if (!grade_needs_regrade_final_grades($course->id)) {
389
        return false;
390
    }
391
 
392
    if (grade_needs_regrade_progress_bar($course->id)) {
1441 ariadna 393
        // Queue ad-hoc task and redirect.
394
        grade_regrade_final_grades($course->id, async: true);
395
        return $callback ? call_user_func($callback) : $PAGE->url;
1 efrain 396
    } else {
397
        $result = grade_regrade_final_grades($course->id);
398
        if ($callback) {
399
            call_user_func($callback);
400
        }
401
        return $result;
402
    }
403
}
404
 
405
/**
406
 * Returns grading information for given activity, optionally with user grades.
407
 * Manual, course or category items can not be queried.
408
 *
409
 * This function can be VERY costly - it is doing full course grades recalculation if needsupdate = 1
410
 * for course grade item. So be sure you really need it.
411
 * If you need just certain grades consider using grade_item::refresh_grades()
412
 * together with grade_item::get_grade() instead.
413
 *
414
 * @param int    $courseid ID of course
415
 * @param string $itemtype Type of grade item. For example, 'mod' or 'block'
416
 * @param string $itemmodule More specific then $itemtype. For example, 'forum' or 'quiz'. May be NULL for some item types
417
 * @param int    $iteminstance ID of the item module
418
 * @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
419
 * @return stdClass Object with keys {items, outcomes, errors}, where 'items' is an array of grade
420
 *               information objects (scaleid, name, grade and locked status, etc.) indexed with itemnumbers
421
 * @category grade
422
 */
423
function grade_get_grades($courseid, $itemtype, $itemmodule, $iteminstance, $userid_or_ids=null) {
424
    global $CFG;
425
 
426
    $return = new stdClass();
427
    $return->items    = array();
428
    $return->outcomes = array();
429
    $return->errors = [];
430
 
431
    $courseitem = grade_item::fetch_course_item($courseid);
432
    $needsupdate = array();
433
    if ($courseitem->needsupdate) {
434
        $result = grade_regrade_final_grades($courseid);
435
        if ($result !== true) {
436
            $needsupdate = array_keys($result);
437
            // Return regrade errors if the user has capability.
438
            $context = context_course::instance($courseid);
439
            if (has_capability('moodle/grade:edit', $context)) {
440
                $return->errors = $result;
441
            }
442
            $courseitem->regrading_finished();
443
        }
444
    }
445
 
446
    if ($grade_items = grade_item::fetch_all(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid))) {
447
        foreach ($grade_items as $grade_item) {
448
            $decimalpoints = null;
449
 
450
            if (empty($grade_item->outcomeid)) {
451
                // prepare information about grade item
452
                $item = new stdClass();
453
                $item->id = $grade_item->id;
454
                $item->itemnumber = $grade_item->itemnumber;
455
                $item->itemtype  = $grade_item->itemtype;
456
                $item->itemmodule = $grade_item->itemmodule;
457
                $item->iteminstance = $grade_item->iteminstance;
458
                $item->scaleid    = $grade_item->scaleid;
459
                $item->name       = $grade_item->get_name();
460
                $item->grademin   = $grade_item->grademin;
461
                $item->grademax   = $grade_item->grademax;
462
                $item->gradepass  = $grade_item->gradepass;
463
                $item->locked     = $grade_item->is_locked();
464
                $item->hidden     = $grade_item->is_hidden();
465
                $item->grades     = array();
466
 
467
                switch ($grade_item->gradetype) {
468
                    case GRADE_TYPE_NONE:
469
                        break;
470
 
471
                    case GRADE_TYPE_VALUE:
472
                        $item->scaleid = 0;
473
                        break;
474
 
475
                    case GRADE_TYPE_TEXT:
476
                        $item->scaleid   = 0;
477
                        $item->grademin   = 0;
478
                        $item->grademax   = 0;
479
                        $item->gradepass  = 0;
480
                        break;
481
                }
482
 
483
                if (empty($userid_or_ids)) {
484
                    $userids = array();
485
 
486
                } else if (is_array($userid_or_ids)) {
487
                    $userids = $userid_or_ids;
488
 
489
                } else {
490
                    $userids = array($userid_or_ids);
491
                }
492
 
493
                if ($userids) {
494
                    $grade_grades = grade_grade::fetch_users_grades($grade_item, $userids, true);
495
                    foreach ($userids as $userid) {
496
                        $grade_grades[$userid]->grade_item =& $grade_item;
497
 
498
                        $grade = new stdClass();
499
                        $grade->grade          = $grade_grades[$userid]->finalgrade;
500
                        $grade->locked         = $grade_grades[$userid]->is_locked();
501
                        $grade->hidden         = $grade_grades[$userid]->is_hidden();
502
                        $grade->overridden     = $grade_grades[$userid]->overridden;
503
                        $grade->feedback       = $grade_grades[$userid]->feedback;
504
                        $grade->feedbackformat = $grade_grades[$userid]->feedbackformat;
505
                        $grade->usermodified   = $grade_grades[$userid]->usermodified;
506
                        $grade->datesubmitted  = $grade_grades[$userid]->get_datesubmitted();
507
                        $grade->dategraded     = $grade_grades[$userid]->get_dategraded();
1441 ariadna 508
                        $grade->deductedmark   = $grade_grades[$userid]->deductedmark;
1 efrain 509
 
510
                        // create text representation of grade
511
                        if ($grade_item->gradetype == GRADE_TYPE_TEXT or $grade_item->gradetype == GRADE_TYPE_NONE) {
512
                            $grade->grade          = null;
513
                            $grade->str_grade      = '-';
514
                            $grade->str_long_grade = $grade->str_grade;
515
 
516
                        } else if (in_array($grade_item->id, $needsupdate)) {
517
                            $grade->grade          = false;
518
                            $grade->str_grade      = get_string('error');
519
                            $grade->str_long_grade = $grade->str_grade;
520
 
521
                        } else if (is_null($grade->grade)) {
522
                            $grade->str_grade      = '-';
523
                            $grade->str_long_grade = $grade->str_grade;
524
 
525
                        } else {
526
                            $grade->str_grade = grade_format_gradevalue($grade->grade, $grade_item);
527
                            if ($grade_item->gradetype == GRADE_TYPE_SCALE or $grade_item->get_displaytype() != GRADE_DISPLAY_TYPE_REAL) {
528
                                $grade->str_long_grade = $grade->str_grade;
529
                            } else {
530
                                $a = new stdClass();
531
                                $a->grade = $grade->str_grade;
532
                                $a->max   = grade_format_gradevalue($grade_item->grademax, $grade_item);
533
                                $grade->str_long_grade = get_string('gradelong', 'grades', $a);
534
                            }
535
                        }
536
 
537
                        // create html representation of feedback
538
                        if (is_null($grade->feedback)) {
539
                            $grade->str_feedback = '';
540
                        } else {
541
                            $feedback = file_rewrite_pluginfile_urls(
542
                                $grade->feedback,
543
                                'pluginfile.php',
544
                                $grade_grades[$userid]->get_context()->id,
545
                                GRADE_FILE_COMPONENT,
546
                                GRADE_FEEDBACK_FILEAREA,
547
                                $grade_grades[$userid]->id
548
                            );
549
 
550
                            $grade->str_feedback = format_text($feedback, $grade->feedbackformat,
551
                                ['context' => $grade_grades[$userid]->get_context()]);
552
                        }
553
 
554
                        $item->grades[$userid] = $grade;
555
                    }
556
                }
557
                $return->items[$grade_item->itemnumber] = $item;
558
 
559
            } else {
560
                if (!$grade_outcome = grade_outcome::fetch(array('id'=>$grade_item->outcomeid))) {
561
                    debugging('Incorect outcomeid found');
562
                    continue;
563
                }
564
 
565
                // outcome info
566
                $outcome = new stdClass();
567
                $outcome->id = $grade_item->id;
568
                $outcome->itemnumber = $grade_item->itemnumber;
569
                $outcome->itemtype   = $grade_item->itemtype;
570
                $outcome->itemmodule = $grade_item->itemmodule;
571
                $outcome->iteminstance = $grade_item->iteminstance;
572
                $outcome->scaleid    = $grade_outcome->scaleid;
573
                $outcome->name       = $grade_outcome->get_name();
574
                $outcome->locked     = $grade_item->is_locked();
575
                $outcome->hidden     = $grade_item->is_hidden();
576
 
577
                if (empty($userid_or_ids)) {
578
                    $userids = array();
579
                } else if (is_array($userid_or_ids)) {
580
                    $userids = $userid_or_ids;
581
                } else {
582
                    $userids = array($userid_or_ids);
583
                }
584
 
585
                if ($userids) {
586
                    $grade_grades = grade_grade::fetch_users_grades($grade_item, $userids, true);
587
                    foreach ($userids as $userid) {
588
                        $grade_grades[$userid]->grade_item =& $grade_item;
589
 
590
                        $grade = new stdClass();
591
                        $grade->grade          = $grade_grades[$userid]->finalgrade;
592
                        $grade->locked         = $grade_grades[$userid]->is_locked();
593
                        $grade->hidden         = $grade_grades[$userid]->is_hidden();
594
                        $grade->feedback       = $grade_grades[$userid]->feedback;
595
                        $grade->feedbackformat = $grade_grades[$userid]->feedbackformat;
596
                        $grade->usermodified   = $grade_grades[$userid]->usermodified;
597
                        $grade->datesubmitted  = $grade_grades[$userid]->get_datesubmitted();
598
                        $grade->dategraded     = $grade_grades[$userid]->get_dategraded();
599
 
600
                        // create text representation of grade
601
                        if (in_array($grade_item->id, $needsupdate)) {
602
                            $grade->grade     = false;
603
                            $grade->str_grade = get_string('error');
604
 
605
                        } else if (is_null($grade->grade)) {
606
                            $grade->grade = 0;
607
                            $grade->str_grade = get_string('nooutcome', 'grades');
608
 
609
                        } else {
610
                            $grade->grade = (int)$grade->grade;
611
                            $scale = $grade_item->load_scale();
612
                            $grade->str_grade = format_string($scale->scale_items[(int)$grade->grade-1]);
613
                        }
614
 
615
                        // create html representation of feedback
616
                        if (is_null($grade->feedback)) {
617
                            $grade->str_feedback = '';
618
                        } else {
619
                            $grade->str_feedback = format_text($grade->feedback, $grade->feedbackformat);
620
                        }
621
 
622
                        $outcome->grades[$userid] = $grade;
623
                    }
624
                }
625
 
626
                if (isset($return->outcomes[$grade_item->itemnumber])) {
627
                    // itemnumber duplicates - lets fix them!
628
                    $newnumber = $grade_item->itemnumber + 1;
629
                    while(grade_item::fetch(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid, 'itemnumber'=>$newnumber))) {
630
                        $newnumber++;
631
                    }
632
                    $outcome->itemnumber    = $newnumber;
633
                    $grade_item->itemnumber = $newnumber;
634
                    $grade_item->update('system');
635
                }
636
 
637
                $return->outcomes[$grade_item->itemnumber] = $outcome;
638
 
639
            }
640
        }
641
    }
642
 
643
    // sort results using itemnumbers
644
    ksort($return->items, SORT_NUMERIC);
645
    ksort($return->outcomes, SORT_NUMERIC);
646
 
647
    return $return;
648
}
649
 
650
///////////////////////////////////////////////////////////////////
651
///// End of public API for communication with modules/blocks /////
652
///////////////////////////////////////////////////////////////////
653
 
654
 
655
 
656
///////////////////////////////////////////////////////////////////
657
///// Internal API: used by gradebook plugins and Moodle core /////
658
///////////////////////////////////////////////////////////////////
659
 
660
/**
661
 * Returns a  course gradebook setting
662
 *
663
 * @param int $courseid
664
 * @param string $name of setting, maybe null if reset only
665
 * @param string $default value to return if setting is not found
666
 * @param bool $resetcache force reset of internal static cache
667
 * @return string value of the setting, $default if setting not found, NULL if supplied $name is null
668
 */
669
function grade_get_setting($courseid, $name, $default=null, $resetcache=false) {
670
    global $DB;
671
 
672
    $cache = cache::make('core', 'gradesetting');
673
    $gradesetting = $cache->get($courseid) ?: array();
674
 
675
    if ($resetcache or empty($gradesetting)) {
676
        $gradesetting = array();
677
        $cache->set($courseid, $gradesetting);
678
 
679
    } else if (is_null($name)) {
680
        return null;
681
 
682
    } else if (array_key_exists($name, $gradesetting)) {
683
        return $gradesetting[$name];
684
    }
685
 
686
    if (!$data = $DB->get_record('grade_settings', array('courseid'=>$courseid, 'name'=>$name))) {
687
        $result = null;
688
    } else {
689
        $result = $data->value;
690
    }
691
 
692
    if (is_null($result)) {
693
        $result = $default;
694
    }
695
 
696
    $gradesetting[$name] = $result;
697
    $cache->set($courseid, $gradesetting);
698
    return $result;
699
}
700
 
701
/**
702
 * Returns all course gradebook settings as object properties
703
 *
704
 * @param int $courseid
705
 * @return object
706
 */
707
function grade_get_settings($courseid) {
708
    global $DB;
709
 
710
     $settings = new stdClass();
711
     $settings->id = $courseid;
712
 
713
    if ($records = $DB->get_records('grade_settings', array('courseid'=>$courseid))) {
714
        foreach ($records as $record) {
715
            $settings->{$record->name} = $record->value;
716
        }
717
    }
718
 
719
    return $settings;
720
}
721
 
722
/**
723
 * Add, update or delete a course gradebook setting
724
 *
725
 * @param int $courseid The course ID
726
 * @param string $name Name of the setting
727
 * @param string $value Value of the setting. NULL means delete the setting.
728
 */
729
function grade_set_setting($courseid, $name, $value) {
730
    global $DB;
731
 
732
    if (is_null($value)) {
733
        $DB->delete_records('grade_settings', array('courseid'=>$courseid, 'name'=>$name));
734
 
735
    } else if (!$existing = $DB->get_record('grade_settings', array('courseid'=>$courseid, 'name'=>$name))) {
736
        $data = new stdClass();
737
        $data->courseid = $courseid;
738
        $data->name     = $name;
739
        $data->value    = $value;
740
        $DB->insert_record('grade_settings', $data);
741
 
742
    } else {
743
        $data = new stdClass();
744
        $data->id       = $existing->id;
745
        $data->value    = $value;
746
        $DB->update_record('grade_settings', $data);
747
    }
748
 
749
    grade_get_setting($courseid, null, null, true); // reset the cache
750
}
751
 
752
/**
753
 * Returns string representation of grade value
754
 *
755
 * @param float|null $value The grade value
756
 * @param object $grade_item Grade item object passed by reference to prevent scale reloading
757
 * @param bool $localized use localised decimal separator
758
 * @param int $displaytype type of display. For example GRADE_DISPLAY_TYPE_REAL, GRADE_DISPLAY_TYPE_PERCENTAGE, GRADE_DISPLAY_TYPE_LETTER
759
 * @param int $decimals The number of decimal places when displaying float values
760
 * @return string
761
 */
762
function grade_format_gradevalue(?float $value, &$grade_item, $localized=true, $displaytype=null, $decimals=null) {
763
    if ($grade_item->gradetype == GRADE_TYPE_NONE or $grade_item->gradetype == GRADE_TYPE_TEXT) {
764
        return '';
765
    }
766
 
767
    // no grade yet?
768
    if (is_null($value)) {
769
        return '-';
770
    }
771
 
772
    if ($grade_item->gradetype != GRADE_TYPE_VALUE and $grade_item->gradetype != GRADE_TYPE_SCALE) {
773
        //unknown type??
774
        return '';
775
    }
776
 
777
    if (is_null($displaytype)) {
778
        $displaytype = $grade_item->get_displaytype();
779
    }
780
 
781
    if (is_null($decimals)) {
782
        $decimals = $grade_item->get_decimals();
783
    }
784
 
785
    switch ($displaytype) {
786
        case GRADE_DISPLAY_TYPE_REAL:
787
            return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized);
788
 
789
        case GRADE_DISPLAY_TYPE_PERCENTAGE:
790
            return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized);
791
 
792
        case GRADE_DISPLAY_TYPE_LETTER:
793
            return grade_format_gradevalue_letter($value, $grade_item);
794
 
795
        case GRADE_DISPLAY_TYPE_REAL_PERCENTAGE:
796
            return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ' (' .
797
                    grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ')';
798
 
799
        case GRADE_DISPLAY_TYPE_REAL_LETTER:
800
            return grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ' (' .
801
                    grade_format_gradevalue_letter($value, $grade_item) . ')';
802
 
803
        case GRADE_DISPLAY_TYPE_PERCENTAGE_REAL:
804
            return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ' (' .
805
                    grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ')';
806
 
807
        case GRADE_DISPLAY_TYPE_LETTER_REAL:
808
            return grade_format_gradevalue_letter($value, $grade_item) . ' (' .
809
                    grade_format_gradevalue_real($value, $grade_item, $decimals, $localized) . ')';
810
 
811
        case GRADE_DISPLAY_TYPE_LETTER_PERCENTAGE:
812
            return grade_format_gradevalue_letter($value, $grade_item) . ' (' .
813
                    grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ')';
814
 
815
        case GRADE_DISPLAY_TYPE_PERCENTAGE_LETTER:
816
            return grade_format_gradevalue_percentage($value, $grade_item, $decimals, $localized) . ' (' .
817
                    grade_format_gradevalue_letter($value, $grade_item) . ')';
818
        default:
819
            return '';
820
    }
821
}
822
 
823
/**
824
 * Returns a float representation of a grade value
825
 *
826
 * @param float|null $value The grade value
827
 * @param object $grade_item Grade item object
828
 * @param int $decimals The number of decimal places
829
 * @param bool $localized use localised decimal separator
830
 * @return string
831
 */
832
function grade_format_gradevalue_real(?float $value, $grade_item, $decimals, $localized) {
833
    if ($grade_item->gradetype == GRADE_TYPE_SCALE) {
834
        if (!$scale = $grade_item->load_scale()) {
835
            return get_string('error');
836
        }
837
 
838
        $value = $grade_item->bounded_grade($value);
839
        return format_string($scale->scale_items[$value-1]);
840
 
841
    } else {
842
        return format_float($value, $decimals, $localized);
843
    }
844
}
845
 
846
/**
847
 * Returns a percentage representation of a grade value
848
 *
849
 * @param float|null $value The grade value
850
 * @param object $grade_item Grade item object
851
 * @param int $decimals The number of decimal places
852
 * @param bool $localized use localised decimal separator
853
 * @return string
854
 */
855
function grade_format_gradevalue_percentage(?float $value, $grade_item, $decimals, $localized) {
856
    $min = $grade_item->grademin;
857
    $max = $grade_item->grademax;
858
    if ($min == $max) {
859
        return '';
860
    }
861
    $value = $grade_item->bounded_grade($value);
862
    $percentage = (($value-$min)*100)/($max-$min);
863
    return format_float($percentage, $decimals, $localized).' %';
864
}
865
 
866
/**
867
 * Returns a letter grade representation of a grade value
868
 * The array of grade letters used is produced by {@link grade_get_letters()} using the course context
869
 *
870
 * @param float|null $value The grade value
871
 * @param object $grade_item Grade item object
872
 * @return string
873
 */
874
function grade_format_gradevalue_letter(?float $value, $grade_item) {
875
    global $CFG;
876
    $context = context_course::instance($grade_item->courseid, IGNORE_MISSING);
877
    if (!$letters = grade_get_letters($context)) {
878
        return ''; // no letters??
879
    }
880
 
881
    if (is_null($value)) {
882
        return '-';
883
    }
884
 
885
    $value = grade_grade::standardise_score($value, $grade_item->grademin, $grade_item->grademax, 0, 100);
886
    $value = bounded_number(0, $value, 100); // just in case
887
 
888
    $gradebookcalculationsfreeze = 'gradebook_calculations_freeze_' . $grade_item->courseid;
889
 
890
    foreach ($letters as $boundary => $letter) {
891
        if (property_exists($CFG, $gradebookcalculationsfreeze) && (int)$CFG->{$gradebookcalculationsfreeze} <= 20160518) {
892
            // Do nothing.
893
        } else {
894
            // The boundary is a percentage out of 100 so use 0 as the min and 100 as the max.
895
            $boundary = grade_grade::standardise_score($boundary, 0, 100, 0, 100);
896
        }
897
        if ($value >= $boundary) {
898
            return format_string($letter);
899
        }
900
    }
901
    return '-'; // no match? maybe '' would be more correct
902
}
903
 
904
 
905
/**
906
 * Returns grade options for gradebook grade category menu
907
 *
908
 * @param int $courseid The course ID
909
 * @param bool $includenew Include option for new category at array index -1
910
 * @return array of grade categories in course
911
 */
912
function grade_get_categories_menu($courseid, $includenew=false) {
913
    $result = array();
914
    if (!$categories = grade_category::fetch_all(array('courseid'=>$courseid))) {
915
        //make sure course category exists
916
        if (!grade_category::fetch_course_category($courseid)) {
917
            debugging('Can not create course grade category!');
918
            return $result;
919
        }
920
        $categories = grade_category::fetch_all(array('courseid'=>$courseid));
921
    }
922
    foreach ($categories as $key=>$category) {
923
        if ($category->is_course_category()) {
924
            $result[$category->id] = get_string('uncategorised', 'grades');
925
            unset($categories[$key]);
926
        }
927
    }
928
    if ($includenew) {
929
        $result[-1] = get_string('newcategory', 'grades');
930
    }
931
    $cats = array();
932
    foreach ($categories as $category) {
933
        $cats[$category->id] = $category->get_name();
934
    }
935
    core_collator::asort($cats);
936
 
937
    return ($result+$cats);
938
}
939
 
940
/**
941
 * Returns the array of grade letters to be used in the supplied context
942
 *
943
 * @param object $context Context object or null for defaults
944
 * @return array of grade_boundary (minimum) => letter_string
945
 */
946
function grade_get_letters($context=null) {
947
    global $DB;
948
 
949
    if (empty($context)) {
950
        //default grading letters
951
        return array('93'=>'A', '90'=>'A-', '87'=>'B+', '83'=>'B', '80'=>'B-', '77'=>'C+', '73'=>'C', '70'=>'C-', '67'=>'D+', '60'=>'D', '0'=>'F');
952
    }
953
 
954
    $cache = cache::make('core', 'grade_letters');
955
    $data = $cache->get($context->id);
956
 
957
    if (!empty($data)) {
958
        return $data;
959
    }
960
 
961
    $letters = array();
962
 
963
    $contexts = $context->get_parent_context_ids();
964
    array_unshift($contexts, $context->id);
965
 
966
    foreach ($contexts as $ctxid) {
967
        if ($records = $DB->get_records('grade_letters', array('contextid'=>$ctxid), 'lowerboundary DESC')) {
968
            foreach ($records as $record) {
969
                $letters[$record->lowerboundary] = $record->letter;
970
            }
971
        }
972
 
973
        if (!empty($letters)) {
974
            // Cache the grade letters for this context.
975
            $cache->set($context->id, $letters);
976
            return $letters;
977
        }
978
    }
979
 
980
    $letters = grade_get_letters(null);
981
    // Cache the grade letters for this context.
982
    $cache->set($context->id, $letters);
983
    return $letters;
984
}
985
 
986
 
987
/**
988
 * Verify new value of grade item idnumber. Checks for uniqueness of new ID numbers. Old ID numbers are kept intact.
989
 *
990
 * @param string $idnumber string (with magic quotes)
991
 * @param int $courseid ID numbers are course unique only
992
 * @param grade_item $grade_item The grade item this idnumber is associated with
993
 * @param stdClass $cm used for course module idnumbers and items attached to modules
994
 * @return bool true means idnumber ok
995
 */
996
function grade_verify_idnumber($idnumber, $courseid, $grade_item=null, $cm=null) {
997
    global $DB;
998
 
999
    if ($idnumber == '') {
1000
        //we allow empty idnumbers
1001
        return true;
1002
    }
1003
 
1004
    // keep existing even when not unique
1005
    if ($cm and $cm->idnumber == $idnumber) {
1006
        if ($grade_item and $grade_item->itemnumber != 0) {
1007
            // grade item with itemnumber > 0 can't have the same idnumber as the main
1008
            // itemnumber 0 which is synced with course_modules
1009
            return false;
1010
        }
1011
        return true;
1012
    } else if ($grade_item and $grade_item->idnumber == $idnumber) {
1013
        return true;
1014
    }
1015
 
1016
    if ($DB->record_exists('course_modules', array('course'=>$courseid, 'idnumber'=>$idnumber))) {
1017
        return false;
1018
    }
1019
 
1020
    if ($DB->record_exists('grade_items', array('courseid'=>$courseid, 'idnumber'=>$idnumber))) {
1021
        return false;
1022
    }
1023
 
1024
    return true;
1025
}
1026
 
1027
/**
1028
 * Force final grade recalculation in all course items
1029
 *
1030
 * @param int $courseid The course ID to recalculate
1031
 */
1032
function grade_force_full_regrading($courseid) {
1033
    global $DB;
1034
    $DB->set_field('grade_items', 'needsupdate', 1, array('courseid'=>$courseid));
1035
}
1036
 
1037
/**
1038
 * Forces regrading of all site grades. Used when changing site setings
1039
 */
1040
function grade_force_site_regrading() {
1041
    global $CFG, $DB;
1042
    $DB->set_field('grade_items', 'needsupdate', 1);
1043
}
1044
 
1045
/**
1046
 * Recover a user's grades from grade_grades_history
1047
 * @param int $userid the user ID whose grades we want to recover
1048
 * @param int $courseid the relevant course
1049
 * @return bool true if successful or false if there was an error or no grades could be recovered
1050
 */
1051
function grade_recover_history_grades($userid, $courseid) {
1052
    global $CFG, $DB;
1053
 
1054
    if ($CFG->disablegradehistory) {
1055
        debugging('Attempting to recover grades when grade history is disabled.');
1056
        return false;
1057
    }
1058
 
1059
    //Were grades recovered? Flag to return.
1060
    $recoveredgrades = false;
1061
 
1062
    //Check the user is enrolled in this course
1063
    //Dont bother checking if they have a gradeable role. They may get one later so recover
1064
    //whatever grades they have now just in case.
1065
    $course_context = context_course::instance($courseid);
1066
    if (!is_enrolled($course_context, $userid)) {
1067
        debugging('Attempting to recover the grades of a user who is deleted or not enrolled. Skipping recover.');
1068
        return false;
1069
    }
1070
 
1071
    //Check for existing grades for this user in this course
1072
    //Recovering grades when the user already has grades can lead to duplicate indexes and bad data
1073
    //In the future we could move the existing grades to the history table then recover the grades from before then
1074
    $sql = "SELECT gg.id
1075
              FROM {grade_grades} gg
1076
              JOIN {grade_items} gi ON gi.id = gg.itemid
1077
             WHERE gi.courseid = :courseid AND gg.userid = :userid";
1078
    $params = array('userid' => $userid, 'courseid' => $courseid);
1079
    if ($DB->record_exists_sql($sql, $params)) {
1080
        debugging('Attempting to recover the grades of a user who already has grades. Skipping recover.');
1081
        return false;
1082
    } else {
1083
        //Retrieve the user's old grades
1084
        //have history ID as first column to guarantee we a unique first column
1085
        $sql = "SELECT h.id, gi.itemtype, gi.itemmodule, gi.iteminstance as iteminstance, gi.itemnumber, h.source, h.itemid, h.userid, h.rawgrade, h.rawgrademax,
1086
                       h.rawgrademin, h.rawscaleid, h.usermodified, h.finalgrade, h.hidden, h.locked, h.locktime, h.exported, h.overridden, h.excluded, h.feedback,
1087
                       h.feedbackformat, h.information, h.informationformat, h.timemodified, itemcreated.tm AS timecreated
1088
                  FROM {grade_grades_history} h
1089
                  JOIN (SELECT itemid, MAX(id) AS id
1090
                          FROM {grade_grades_history}
1091
                         WHERE userid = :userid1
1092
                      GROUP BY itemid) maxquery ON h.id = maxquery.id AND h.itemid = maxquery.itemid
1093
                  JOIN {grade_items} gi ON gi.id = h.itemid
1094
                  JOIN (SELECT itemid, MAX(timemodified) AS tm
1095
                          FROM {grade_grades_history}
1096
                         WHERE userid = :userid2 AND action = :insertaction
1097
                      GROUP BY itemid) itemcreated ON itemcreated.itemid = h.itemid
1098
                 WHERE gi.courseid = :courseid";
1099
        $params = array('userid1' => $userid, 'userid2' => $userid , 'insertaction' => GRADE_HISTORY_INSERT, 'courseid' => $courseid);
1100
        $oldgrades = $DB->get_records_sql($sql, $params);
1101
 
1102
        //now move the old grades to the grade_grades table
1103
        foreach ($oldgrades as $oldgrade) {
1104
            unset($oldgrade->id);
1105
 
1106
            $grade = new grade_grade($oldgrade, false);//2nd arg false as dont want to try and retrieve a record from the DB
1107
            $grade->insert($oldgrade->source);
1108
 
1109
            //dont include default empty grades created when activities are created
1110
            if (!is_null($oldgrade->finalgrade) || !is_null($oldgrade->feedback)) {
1111
                $recoveredgrades = true;
1112
            }
1113
        }
1114
    }
1115
 
1116
    //Some activities require manual grade synching (moving grades from the activity into the gradebook)
1117
    //If the student was deleted when synching was done they may have grades in the activity that haven't been moved across
1118
    grade_grab_course_grades($courseid, null, $userid);
1119
 
1120
    return $recoveredgrades;
1121
}
1122
 
1123
/**
1124
 * Updates all final grades in course.
1125
 *
1126
 * @param int $courseid The course ID
1127
 * @param int $userid If specified try to do a quick regrading of the grades of this user only
1128
 * @param object $updated_item Optional grade item to be marked for regrading. It is required if $userid is set.
1129
 * @param \core\progress\base $progress If provided, will be used to update progress on this long operation.
1441 ariadna 1130
 * @param bool $async If true, and we are recalculating an entire course's grades, defer processing to an ad-hoc task.
1 efrain 1131
 * @return array|true true if ok, array of errors if problems found. Grade item id => error message
1132
 */
1441 ariadna 1133
function grade_regrade_final_grades($courseid, $userid=null, $updated_item=null, $progress=null, bool $async = false) {
1 efrain 1134
    // This may take a very long time and extra memory.
1135
    \core_php_time_limit::raise();
1136
    raise_memory_limit(MEMORY_EXTRA);
1137
 
1138
    $course_item = grade_item::fetch_course_item($courseid);
1139
 
1140
    if ($progress == null) {
1141
        $progress = new \core\progress\none();
1142
    }
1143
 
1144
    if ($userid) {
1145
        // one raw grade updated for one user
1146
        if (empty($updated_item)) {
1147
            throw new \moodle_exception("cannotbenull", 'debug', '', "updated_item");
1148
        }
1149
        if ($course_item->needsupdate) {
1150
            $updated_item->force_regrading();
1151
            return array($course_item->id =>'Can not do fast regrading after updating of raw grades');
1152
        }
1153
 
1154
    } else {
1155
        if (!$course_item->needsupdate) {
1156
            // nothing to do :-)
1441 ariadna 1157
            if ($progress instanceof \core\progress\stored) {
1158
                // The regrade was already run elsewhere without the stored progress, so just start and end it now.
1159
                $progress->start_progress(get_string('recalculatinggrades', 'grades'));
1160
                $progress->end_progress();
1161
            }
1 efrain 1162
            return true;
1163
        }
1441 ariadna 1164
        // Defer recalculation to an ad-hoc task.
1165
        if ($async) {
1166
            $regradecache = cache::make_from_params(
1167
                mode: cache_store::MODE_REQUEST,
1168
                component: 'core',
1169
                area: 'grade_regrade_final_grades',
1170
                options: [
1171
                    'simplekeys' => true,
1172
                    'simpledata' => true,
1173
                ],
1174
            );
1175
            // If the courseid already exists in the cache, return so we don't do this multiple times per request.
1176
            if ($regradecache->get($courseid)) {
1177
                return true;
1178
            }
1179
            $task = \core_course\task\regrade_final_grades::create($courseid);
1180
            $taskid = \core\task\manager::queue_adhoc_task($task, true);
1181
            if ($taskid) {
1182
                $task->set_id($taskid);
1183
                $task->initialise_stored_progress();
1184
            }
1185
            $regradecache->set($courseid, true);
1186
            return true;
1187
        }
1 efrain 1188
    }
1189
 
1190
    // Categories might have to run some processing before we fetch the grade items.
1191
    // This gives them a final opportunity to update and mark their children to be updated.
1192
    // We need to work on the children categories up to the parent ones, so that, for instance,
1193
    // if a category total is updated it will be reflected in the parent category.
1194
    $cats = grade_category::fetch_all(array('courseid' => $courseid));
1195
    $flatcattree = array();
1196
    foreach ($cats as $cat) {
1197
        if (!isset($flatcattree[$cat->depth])) {
1198
            $flatcattree[$cat->depth] = array();
1199
        }
1200
        $flatcattree[$cat->depth][] = $cat;
1201
    }
1202
    krsort($flatcattree);
1203
    foreach ($flatcattree as $depth => $cats) {
1204
        foreach ($cats as $cat) {
1205
            $cat->pre_regrade_final_grades();
1206
        }
1207
    }
1208
 
1209
    $progresstotal = 0;
1210
    $progresscurrent = 0;
1211
 
1212
    $grade_items = grade_item::fetch_all(array('courseid'=>$courseid));
1213
    $depends_on = array();
1214
 
1215
    foreach ($grade_items as $gid=>$gitem) {
1216
        if ((!empty($updated_item) and $updated_item->id == $gid) ||
1217
                $gitem->is_course_item() || $gitem->is_category_item() || $gitem->is_calculated()) {
1218
            $grade_items[$gid]->needsupdate = 1;
1219
        }
1220
 
1221
        // We load all dependencies of these items later we can discard some grade_items based on this.
1222
        if ($grade_items[$gid]->needsupdate) {
1223
            $depends_on[$gid] = $grade_items[$gid]->depends_on();
1224
            $progresstotal++;
1225
        }
1226
    }
1227
 
1441 ariadna 1228
    $progress->start_progress(get_string('recalculatinggrades', 'grades'), $progresstotal);
1 efrain 1229
 
1230
    $errors = array();
1231
    $finalids = array();
1232
    $updatedids = array();
1233
    $gids     = array_keys($grade_items);
1234
    $failed = 0;
1235
 
1236
    while (count($finalids) < count($gids)) { // work until all grades are final or error found
1237
        $count = 0;
1238
        foreach ($gids as $gid) {
1239
            if (in_array($gid, $finalids)) {
1240
                continue; // already final
1241
            }
1242
 
1243
            if (!$grade_items[$gid]->needsupdate) {
1244
                $finalids[] = $gid; // we can make it final - does not need update
1245
                continue;
1246
            }
1247
            $thisprogress = $progresstotal;
1248
            foreach ($grade_items as $item) {
1249
                if ($item->needsupdate) {
1250
                    $thisprogress--;
1251
                }
1252
            }
1253
            // Clip between $progresscurrent and $progresstotal.
1254
            $thisprogress = max(min($thisprogress, $progresstotal), $progresscurrent);
1255
            $progress->progress($thisprogress);
1256
            $progresscurrent = $thisprogress;
1257
 
1258
            foreach ($depends_on[$gid] as $did) {
1259
                if (!in_array($did, $finalids)) {
1260
                    // This item depends on something that is not yet in finals array.
1261
                    continue 2;
1262
                }
1263
            }
1264
 
1265
            // If this grade item has no dependancy with any updated item at all, then remove it from being recalculated.
1266
 
1267
            // When we get here, all of this grade item's decendents are marked as final so they would be marked as updated too
1268
            // if they would have been regraded. We don't need to regrade items which dependants (not only the direct ones
1269
            // but any dependant in the cascade) have not been updated.
1270
 
1271
            // If $updated_item was specified we discard the grade items that do not depend on it or on any grade item that
1272
            // depend on $updated_item.
1273
 
1274
            // Here we check to see if the direct decendants are marked as updated.
1275
            if (!empty($updated_item) && $gid != $updated_item->id && !in_array($updated_item->id, $depends_on[$gid])) {
1276
 
1277
                // We need to ensure that none of this item's dependencies have been updated.
1278
                // If we find that one of the direct decendants of this grade item is marked as updated then this
1279
                // grade item needs to be recalculated and marked as updated.
1280
                // Being marked as updated is done further down in the code.
1281
 
1282
                $updateddependencies = false;
1283
                foreach ($depends_on[$gid] as $dependency) {
1284
                    if (in_array($dependency, $updatedids)) {
1285
                        $updateddependencies = true;
1286
                        break;
1287
                    }
1288
                }
1289
                if ($updateddependencies === false) {
1290
                    // If no direct descendants are marked as updated, then we don't need to update this grade item. We then mark it
1291
                    // as final.
1292
                    $count++;
1293
                    $finalids[] = $gid;
1294
                    continue;
1295
                }
1296
            }
1297
 
1298
            // Let's update, calculate or aggregate.
1299
            $result = $grade_items[$gid]->regrade_final_grades($userid, $progress);
1300
 
1301
            if ($result === true) {
1302
 
1303
                // We should only update the database if we regraded all users.
1304
                if (empty($userid)) {
1305
                    $grade_items[$gid]->regrading_finished();
1306
                    // Do the locktime item locking.
1307
                    $grade_items[$gid]->check_locktime();
1308
                } else {
1309
                    $grade_items[$gid]->needsupdate = 0;
1310
                }
1311
                $count++;
1312
                $finalids[] = $gid;
1313
                $updatedids[] = $gid;
1314
 
1315
            } else {
1316
                $grade_items[$gid]->force_regrading();
1317
                $errors[$gid] = $result;
1318
            }
1319
        }
1320
 
1321
        if ($count == 0) {
1322
            $failed++;
1323
        } else {
1324
            $failed = 0;
1325
        }
1326
 
1327
        if ($failed > 1) {
1328
            foreach($gids as $gid) {
1329
                if (in_array($gid, $finalids)) {
1330
                    continue; // this one is ok
1331
                }
1332
                $grade_items[$gid]->force_regrading();
1333
                if (!empty($grade_items[$gid]->calculation) && empty($errors[$gid])) {
1334
                    $itemname = $grade_items[$gid]->get_name();
1335
                    $errors[$gid] = get_string('errorcalculationbroken', 'grades', $itemname);
1336
                }
1337
            }
1338
            break; // Found error.
1339
        }
1340
    }
1341
    $progress->end_progress();
1342
 
1343
    if (count($errors) == 0) {
1344
        if (empty($userid)) {
1345
            // do the locktime locking of grades, but only when doing full regrading
1346
            grade_grade::check_locktime_all($gids);
1347
        }
1348
        return true;
1349
    } else {
1350
        return $errors;
1351
    }
1352
}
1353
 
1354
/**
1355
 * Refetches grade data from course activities
1356
 *
1357
 * @param int $courseid The course ID
1358
 * @param string $modname Limit the grade fetch to a single module type. For example 'forum'
1359
 * @param int $userid limit the grade fetch to a single user
1360
 */
1361
function grade_grab_course_grades($courseid, $modname=null, $userid=0) {
1362
    global $CFG, $DB;
1363
 
1364
    if ($modname) {
1365
        $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname
1366
                  FROM {".$modname."} a, {course_modules} cm, {modules} m
1367
                 WHERE m.name=:modname AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=:courseid";
1368
        $params = array('modname'=>$modname, 'courseid'=>$courseid);
1369
 
1370
        if ($modinstances = $DB->get_records_sql($sql, $params)) {
1371
            foreach ($modinstances as $modinstance) {
1372
                grade_update_mod_grades($modinstance, $userid);
1373
            }
1374
        }
1375
        return;
1376
    }
1377
 
1378
    if (!$mods = core_component::get_plugin_list('mod') ) {
1379
        throw new \moodle_exception('nomodules', 'debug');
1380
    }
1381
 
1382
    foreach ($mods as $mod => $fullmod) {
1383
        if ($mod == 'NEWMODULE') {   // Someone has unzipped the template, ignore it
1384
            continue;
1385
        }
1386
 
1387
        // include the module lib once
1388
        if (file_exists($fullmod.'/lib.php')) {
1389
            // get all instance of the activity
1390
            $sql = "SELECT a.*, cm.idnumber as cmidnumber, m.name as modname
1391
                      FROM {".$mod."} a, {course_modules} cm, {modules} m
1392
                     WHERE m.name=:mod AND m.visible=1 AND m.id=cm.module AND cm.instance=a.id AND cm.course=:courseid";
1393
            $params = array('mod'=>$mod, 'courseid'=>$courseid);
1394
 
1395
            if ($modinstances = $DB->get_records_sql($sql, $params)) {
1396
                foreach ($modinstances as $modinstance) {
1397
                    grade_update_mod_grades($modinstance, $userid);
1398
                }
1399
            }
1400
        }
1401
    }
1402
}
1403
 
1404
/**
1405
 * Force full update of module grades in central gradebook
1406
 *
1407
 * @param object $modinstance Module object with extra cmidnumber and modname property
1408
 * @param int $userid Optional user ID if limiting the update to a single user
1409
 * @return bool True if success
1410
 */
1411
function grade_update_mod_grades($modinstance, $userid=0) {
1412
    global $CFG, $DB;
1413
 
1414
    $fullmod = $CFG->dirroot.'/mod/'.$modinstance->modname;
1415
    if (!file_exists($fullmod.'/lib.php')) {
1416
        debugging('missing lib.php file in module ' . $modinstance->modname);
1417
        return false;
1418
    }
1419
    include_once($fullmod.'/lib.php');
1420
 
1421
    $updateitemfunc   = $modinstance->modname.'_grade_item_update';
1422
    $updategradesfunc = $modinstance->modname.'_update_grades';
1423
 
1424
    if (function_exists($updategradesfunc) and function_exists($updateitemfunc)) {
1425
        //new grading supported, force updating of grades
1426
        $updateitemfunc($modinstance);
1427
        $updategradesfunc($modinstance, $userid);
1428
    } else if (function_exists($updategradesfunc) xor function_exists($updateitemfunc)) {
1429
        // Module does not support grading?
1430
        debugging("You have declared one of $updateitemfunc and $updategradesfunc but not both. " .
1431
                  "This will cause broken behaviour.", DEBUG_DEVELOPER);
1432
    }
1433
 
1434
    return true;
1435
}
1436
 
1437
/**
1438
 * Remove grade letters for given context
1439
 *
1440
 * @param context $context The context
1441
 * @param bool $showfeedback If true a success notification will be displayed
1442
 */
1443
function remove_grade_letters($context, $showfeedback) {
1444
    global $DB, $OUTPUT;
1445
 
1446
    $strdeleted = get_string('deleted');
1447
 
1448
    $records = $DB->get_records('grade_letters', array('contextid' => $context->id));
1449
    foreach ($records as $record) {
1450
        $DB->delete_records('grade_letters', array('id' => $record->id));
1451
        // Trigger the letter grade deleted event.
1452
        $event = \core\event\grade_letter_deleted::create(array(
1453
            'objectid' => $record->id,
1454
            'context' => $context,
1455
        ));
1456
        $event->trigger();
1457
    }
1458
    if ($showfeedback) {
1459
        echo $OUTPUT->notification($strdeleted.' - '.get_string('letters', 'grades'), 'notifysuccess');
1460
    }
1461
 
1462
    $cache = cache::make('core', 'grade_letters');
1463
    $cache->delete($context->id);
1464
}
1465
 
1466
/**
1467
 * Remove all grade related course data
1468
 * Grade history is kept
1469
 *
1470
 * @param int $courseid The course ID
1471
 * @param bool $showfeedback If true success notifications will be displayed
1472
 */
1473
function remove_course_grades($courseid, $showfeedback) {
1474
    global $DB, $OUTPUT;
1475
 
1476
    $fs = get_file_storage();
1477
    $strdeleted = get_string('deleted');
1478
 
1479
    $course_category = grade_category::fetch_course_category($courseid);
1480
    $course_category->delete('coursedelete');
1481
    $fs->delete_area_files(context_course::instance($courseid)->id, 'grade', 'feedback');
1482
    if ($showfeedback) {
1483
        echo $OUTPUT->notification($strdeleted.' - '.get_string('grades', 'grades').', '.get_string('items', 'grades').', '.get_string('categories', 'grades'), 'notifysuccess');
1484
    }
1485
 
1486
    if ($outcomes = grade_outcome::fetch_all(array('courseid'=>$courseid))) {
1487
        foreach ($outcomes as $outcome) {
1488
            $outcome->delete('coursedelete');
1489
        }
1490
    }
1491
    $DB->delete_records('grade_outcomes_courses', array('courseid'=>$courseid));
1492
    if ($showfeedback) {
1493
        echo $OUTPUT->notification($strdeleted.' - '.get_string('outcomes', 'grades'), 'notifysuccess');
1494
    }
1495
 
1496
    if ($scales = grade_scale::fetch_all(array('courseid'=>$courseid))) {
1497
        foreach ($scales as $scale) {
1498
            $scale->delete('coursedelete');
1499
        }
1500
    }
1501
    if ($showfeedback) {
1502
        echo $OUTPUT->notification($strdeleted.' - '.get_string('scales'), 'notifysuccess');
1503
    }
1504
 
1505
    $DB->delete_records('grade_settings', array('courseid'=>$courseid));
1506
    if ($showfeedback) {
1507
        echo $OUTPUT->notification($strdeleted.' - '.get_string('settings', 'grades'), 'notifysuccess');
1508
    }
1509
}
1510
 
1511
/**
1512
 * Called when course category is deleted
1513
 * Cleans the gradebook of associated data
1514
 *
1515
 * @param int $categoryid The course category id
1516
 * @param int $newparentid If empty everything is deleted. Otherwise the ID of the category where content moved
1517
 * @param bool $showfeedback print feedback
1518
 */
1519
function grade_course_category_delete($categoryid, $newparentid, $showfeedback) {
1520
    global $DB;
1521
 
1522
    $context = context_coursecat::instance($categoryid);
1523
    $records = $DB->get_records('grade_letters', array('contextid' => $context->id));
1524
    foreach ($records as $record) {
1525
        $DB->delete_records('grade_letters', array('id' => $record->id));
1526
        // Trigger the letter grade deleted event.
1527
        $event = \core\event\grade_letter_deleted::create(array(
1528
            'objectid' => $record->id,
1529
            'context' => $context,
1530
        ));
1531
        $event->trigger();
1532
    }
1533
}
1534
 
1535
/**
1536
 * Does gradebook cleanup when a module is uninstalled
1537
 * Deletes all associated grade items
1538
 *
1539
 * @param string $modname The grade item module name to remove. For example 'forum'
1540
 */
1541
function grade_uninstalled_module($modname) {
1542
    global $CFG, $DB;
1543
 
1544
    $sql = "SELECT *
1545
              FROM {grade_items}
1546
             WHERE itemtype='mod' AND itemmodule=?";
1547
 
1548
    // go all items for this module and delete them including the grades
1549
    $rs = $DB->get_recordset_sql($sql, array($modname));
1550
    foreach ($rs as $item) {
1551
        $grade_item = new grade_item($item, false);
1552
        $grade_item->delete('moduninstall');
1553
    }
1554
    $rs->close();
1555
}
1556
 
1557
/**
1558
 * Deletes all of a user's grade data from gradebook
1559
 *
1560
 * @param int $userid The user whose grade data should be deleted
1561
 */
1562
function grade_user_delete($userid) {
1563
    if ($grades = grade_grade::fetch_all(array('userid'=>$userid))) {
1564
        foreach ($grades as $grade) {
1565
            $grade->delete('userdelete');
1566
        }
1567
    }
1568
}
1569
 
1570
/**
1571
 * Purge course data when user unenrolls from a course
1572
 *
1573
 * @param int $courseid The ID of the course the user has unenrolled from
1574
 * @param int $userid The ID of the user unenrolling
1575
 */
1576
function grade_user_unenrol($courseid, $userid) {
1577
    if ($items = grade_item::fetch_all(array('courseid'=>$courseid))) {
1578
        foreach ($items as $item) {
1579
            if ($grades = grade_grade::fetch_all(array('userid'=>$userid, 'itemid'=>$item->id))) {
1580
                foreach ($grades as $grade) {
1581
                    $grade->delete('userdelete');
1582
                }
1583
            }
1584
        }
1585
    }
1586
}
1587
 
1588
/**
1589
 * Reset all course grades, refetch from the activities and recalculate
1590
 *
1591
 * @param int $courseid The course to reset
1592
 * @return bool success
1593
 */
1594
function grade_course_reset($courseid) {
1595
 
1596
    // no recalculations
1597
    grade_force_full_regrading($courseid);
1598
 
1599
    $grade_items = grade_item::fetch_all(array('courseid'=>$courseid));
1600
    foreach ($grade_items as $gid=>$grade_item) {
1601
        $grade_item->delete_all_grades('reset');
1602
    }
1603
 
1604
    //refetch all grades
1605
    grade_grab_course_grades($courseid);
1606
 
1607
    // recalculate all grades
1441 ariadna 1608
    grade_regrade_final_grades($courseid, async: true);
1 efrain 1609
    return true;
1610
}
1611
 
1612
/**
1613
 * Convert a number to 5 decimal point float, null db compatible format
1614
 * (we need this to decide if db value changed)
1615
 *
1616
 * @param float|null $number The number to convert
1617
 * @return float|null float or null
1618
 */
1619
function grade_floatval(?float $number) {
1620
    if (is_null($number)) {
1621
        return null;
1622
    }
1623
    // we must round to 5 digits to get the same precision as in 10,5 db fields
1624
    // note: db rounding for 10,5 is different from php round() function
1625
    return round($number, 5);
1626
}
1627
 
1628
/**
1629
 * Compare two float numbers safely. Uses 5 decimals php precision using {@link grade_floatval()}. Nulls accepted too.
1630
 * Used for determining if a database update is required
1631
 *
1632
 * @param float|null $f1 Float one to compare
1633
 * @param float|null $f2 Float two to compare
1634
 * @return bool True if the supplied values are different
1635
 */
1636
function grade_floats_different(?float $f1, ?float $f2): bool {
1637
    // note: db rounding for 10,5 is different from php round() function
1638
    return (grade_floatval($f1) !== grade_floatval($f2));
1639
}
1640
 
1641
/**
1642
 * Compare two float numbers safely. Uses 5 decimals php precision using {@link grade_floatval()}
1643
 *
1644
 * Do not use rounding for 10,5 at the database level as the results may be
1645
 * different from php round() function.
1646
 *
1647
 * @since Moodle 2.0
1648
 * @param float|null $f1 Float one to compare
1649
 * @param float|null $f2 Float two to compare
1650
 * @return bool True if the values should be considered as the same grades
1651
 */
1652
function grade_floats_equal(?float $f1, ?float $f2): bool {
1653
    return (grade_floatval($f1) === grade_floatval($f2));
1654
}
1655
 
1656
/**
1657
 * Get the most appropriate grade date for a grade item given the user that the grade relates to.
1658
 *
1659
 * @param \stdClass $grade
1660
 * @param \stdClass $user
1661
 * @return int|null
1662
 */
1663
function grade_get_date_for_user_grade(\stdClass $grade, \stdClass $user): ?int {
1664
    // The `datesubmitted` is the time that the grade was created.
1665
    // The `dategraded` is the time that it was modified or overwritten.
1666
    // If the grade was last modified by the user themselves use the date graded.
1667
    // Otherwise use date submitted.
1668
    if ($grade->usermodified == $user->id || empty($grade->datesubmitted)) {
1669
        return $grade->dategraded;
1670
    } else {
1671
        return $grade->datesubmitted;
1672
    }
1673
}