Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
/**
18
 * Definition of the grader report class
19
 *
20
 * @package   gradereport_grader
21
 * @copyright 2007 Nicolas Connault
22
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
require_once($CFG->dirroot . '/grade/report/lib.php');
26
require_once($CFG->libdir.'/tablelib.php');
27
 
1441 ariadna 28
use core_grades\penalty_manager;
29
 
1 efrain 30
/**
31
 * Class providing an API for the grader report building and displaying.
32
 * @uses grade_report
33
 * @copyright 2007 Nicolas Connault
34
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
35
 */
36
class grade_report_grader extends grade_report {
37
    /**
38
     * The final grades.
39
     * @var array $grades
40
     */
41
    public $grades;
42
 
43
    /**
44
     * Contains all the grades for the course - even the ones not displayed in the grade tree.
45
     *
46
     * @var array $allgrades
47
     */
48
    private $allgrades;
49
 
50
    /**
51
     * Contains all grade items expect GRADE_TYPE_NONE.
52
     *
53
     * @var array $allgradeitems
54
     */
55
    private $allgradeitems;
56
 
57
    /**
58
     * Array of errors for bulk grades updating.
59
     * @var array $gradeserror
60
     */
61
    public $gradeserror = array();
62
 
63
    // SQL-RELATED
64
 
65
    /**
66
     * The id of the grade_item by which this report will be sorted.
67
     * @var int $sortitemid
68
     */
69
    public $sortitemid;
70
 
71
    /**
72
     * Sortorder used in the SQL selections.
73
     * @var int $sortorder
74
     */
75
    public $sortorder;
76
 
77
    /**
78
     * An SQL fragment affecting the search for users.
79
     * @var string $userselect
80
     */
81
    public $userselect;
82
 
83
    /**
84
     * The bound params for $userselect
85
     * @var array $userselectparams
86
     */
87
    public $userselectparams = array();
88
 
89
    /**
90
     * List of collapsed categories from user preference
91
     * @var array $collapsed
92
     */
93
    public $collapsed;
94
 
95
    /**
96
     * A count of the rows, used for css classes.
97
     * @var int $rowcount
98
     */
99
    public $rowcount = 0;
100
 
101
    /**
102
     * Capability check caching
103
     * @var boolean $canviewhidden
104
     */
105
    public $canviewhidden;
106
 
1441 ariadna 107
    /**
108
     * @var int Maximum number of students that can be shown on one page
109
     * @deprecated Since Moodle 4.5 MDL-84245. Use grade_report_grader::get_max_students_per_page() instead.
110
     */
111
    #[\core\attribute\deprecated('grade_report_grader::get_max_students_per_page()', since: '4.5', mdl: 'MDL-84245')]
1 efrain 112
    public const MAX_STUDENTS_PER_PAGE = 5000;
113
 
1441 ariadna 114
    /**
115
     * @var int The maximum number of grades that can be shown on one page.
116
     *
117
     * More than this causes issues for the browser due to the size of the page.
118
     */
119
    public const MAX_GRADES_PER_PAGE = 200000;
120
 
1 efrain 121
    /** @var int[] List of available options on the pagination dropdown */
122
    public const PAGINATION_OPTIONS = [20, 100];
123
 
124
    /**
125
     * Allow category grade overriding
126
     * @var bool $overridecat
127
     */
128
    protected $overridecat;
129
 
130
    /** @var array of objects, or empty array if no records were found. */
131
    protected $users = [];
132
 
133
    /**
134
     * Constructor. Sets local copies of user preferences and initialises grade_tree.
135
     * @param int $courseid
136
     * @param object $gpr grade plugin return tracking object
137
     * @param string $context
138
     * @param int $page The current page being viewed (when report is paged)
139
     * @param int $sortitemid The id of the grade_item by which to sort the table
140
     * @param string $sort Sorting direction
141
     */
142
    public function __construct($courseid, $gpr, $context, $page=null, $sortitemid=null, string $sort = '') {
143
        global $CFG;
144
        parent::__construct($courseid, $gpr, $context, $page);
145
 
146
        $this->canviewhidden = has_capability('moodle/grade:viewhidden', context_course::instance($this->course->id));
147
 
148
        // load collapsed settings for this report
149
        $this->collapsed = static::get_collapsed_preferences($this->course->id);
150
 
151
        if (empty($CFG->enableoutcomes)) {
152
            $nooutcomes = false;
153
        } else {
154
            $nooutcomes = get_user_preferences('grade_report_shownooutcomes');
155
        }
156
 
157
        // if user report preference set or site report setting set use it, otherwise use course or site setting
158
        $switch = $this->get_pref('aggregationposition');
159
        if ($switch == '') {
160
            $switch = grade_get_setting($this->courseid, 'aggregationposition', $CFG->grade_aggregationposition);
161
        }
162
 
163
        // Grab the grade_tree for this course
164
        $this->gtree = new grade_tree($this->courseid, true, $switch, $this->collapsed, $nooutcomes);
165
 
166
        $this->sortitemid = $sortitemid;
167
 
168
        // base url for sorting by first/last name
169
 
170
        $this->baseurl = new moodle_url('index.php', array('id' => $this->courseid));
171
 
172
        $studentsperpage = $this->get_students_per_page();
173
        if (!empty($this->page) && !empty($studentsperpage)) {
174
            $this->baseurl->params(array('perpage' => $studentsperpage, 'page' => $this->page));
175
        }
176
 
177
        $this->pbarurl = new moodle_url('/grade/report/grader/index.php', array('id' => $this->courseid));
178
 
179
        $this->setup_groups();
180
        $this->setup_users();
181
        $this->setup_sortitemid($sort);
182
 
183
        $this->overridecat = (bool)get_config('moodle', 'grade_overridecat');
184
    }
185
 
186
    /**
187
     * Processes the data sent by the form (grades).
188
     * Caller is responsible for all access control checks
189
     * @param array $data form submission (with magic quotes)
190
     * @return array empty array if success, array of warnings if something fails.
191
     */
192
    public function process_data($data) {
193
        global $DB;
194
        $warnings = array();
195
 
196
        $separategroups = false;
197
        $mygroups       = array();
198
        if ($this->groupmode == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $this->context)) {
199
            $separategroups = true;
200
            $mygroups = groups_get_user_groups($this->course->id);
201
            $mygroups = $mygroups[0]; // ignore groupings
202
            // reorder the groups fro better perf below
203
            $current = array_search($this->currentgroup, $mygroups);
204
            if ($current !== false) {
205
                unset($mygroups[$current]);
206
                array_unshift($mygroups, $this->currentgroup);
207
            }
208
        }
209
        $viewfullnames = has_capability('moodle/site:viewfullnames', $this->context);
210
 
211
        // always initialize all arrays
212
        $queue = array();
213
 
214
        $this->load_users();
215
        $this->load_final_grades();
216
 
217
        // Were any changes made?
218
        $changedgrades = false;
219
        $timepageload = clean_param($data->timepageload, PARAM_INT);
220
 
221
        foreach ($data as $varname => $students) {
222
 
223
            $needsupdate = false;
224
 
225
            // Skip, not a grade.
226
            if (strpos($varname, 'grade') === 0) {
227
                $datatype = 'grade';
228
            } else {
229
                continue;
230
            }
231
 
232
            foreach ($students as $userid => $items) {
233
                $userid = clean_param($userid, PARAM_INT);
234
                foreach ($items as $itemid => $postedvalue) {
235
                    $itemid = clean_param($itemid, PARAM_INT);
236
 
237
                    // Was change requested?
238
                    $oldvalue = $this->grades[$userid][$itemid];
239
                    if ($datatype === 'grade') {
240
                        // If there was no grade and there still isn't
241
                        if (is_null($oldvalue->finalgrade) && $postedvalue == -1) {
242
                            // -1 means no grade
243
                            continue;
244
                        }
245
 
246
                        // If the grade item uses a custom scale
247
                        if (!empty($oldvalue->grade_item->scaleid)) {
248
 
249
                            if ((int)$oldvalue->finalgrade === (int)$postedvalue) {
250
                                continue;
251
                            }
252
                        } else {
253
                            // The grade item uses a numeric scale
254
 
255
                            // Format the finalgrade from the DB so that it matches the grade from the client
256
                            if ($postedvalue === format_float($oldvalue->finalgrade, $oldvalue->grade_item->get_decimals())) {
257
                                continue;
258
                            }
259
                        }
260
 
261
                        $changedgrades = true;
262
                    }
263
 
264
                    if (!$gradeitem = grade_item::fetch(array('id'=>$itemid, 'courseid'=>$this->courseid))) {
265
                        throw new \moodle_exception('invalidgradeitemid');
266
                    }
267
 
268
                    // Pre-process grade
269
                    if ($datatype == 'grade') {
270
                        if ($gradeitem->gradetype == GRADE_TYPE_SCALE) {
271
                            if ($postedvalue == -1) { // -1 means no grade
272
                                $finalgrade = null;
273
                            } else {
274
                                $finalgrade = $postedvalue;
275
                            }
276
                        } else {
277
                            $finalgrade = unformat_float($postedvalue);
278
                        }
279
 
280
                        $errorstr = '';
281
                        $skip = false;
282
 
283
                        $dategraded = $oldvalue->get_dategraded();
284
                        if (!empty($dategraded) && $timepageload < $dategraded) {
285
                            // Warn if the grade was updated while we were editing this form.
286
                            $errorstr = 'gradewasmodifiedduringediting';
287
                            $skip = true;
288
                        } else if (!is_null($finalgrade)) {
289
                            // Warn if the grade is out of bounds.
290
                            $bounded = $gradeitem->bounded_grade($finalgrade);
291
                            if ($bounded > $finalgrade) {
292
                                $errorstr = 'lessthanmin';
293
                            } else if ($bounded < $finalgrade) {
294
                                $errorstr = 'morethanmax';
295
                            }
296
                        }
297
 
298
                        if ($errorstr) {
299
                            $userfieldsapi = \core_user\fields::for_name();
300
                            $userfields = 'id, ' . $userfieldsapi->get_sql('', false, '', '', false)->selects;
301
                            $user = $DB->get_record('user', array('id' => $userid), $userfields);
302
                            $gradestr = new stdClass();
303
                            $gradestr->username = fullname($user, $viewfullnames);
304
                            $gradestr->itemname = $gradeitem->get_name();
305
                            $warnings[] = get_string($errorstr, 'grades', $gradestr);
306
                            if ($skip) {
307
                                // Skipping the update of this grade it failed the tests above.
308
                                continue;
309
                            }
310
                        }
311
                    }
312
 
313
                    // group access control
314
                    if ($separategroups) {
315
                        // note: we can not use $this->currentgroup because it would fail badly
316
                        //       when having two browser windows each with different group
317
                        $sharinggroup = false;
318
                        foreach ($mygroups as $groupid) {
319
                            if (groups_is_member($groupid, $userid)) {
320
                                $sharinggroup = true;
321
                                break;
322
                            }
323
                        }
324
                        if (!$sharinggroup) {
325
                            // either group membership changed or somebody is hacking grades of other group
326
                            $warnings[] = get_string('errorsavegrade', 'grades');
327
                            continue;
328
                        }
329
                    }
330
 
331
                    $gradeitem->update_final_grade($userid, $finalgrade, 'gradebook', false,
332
                        FORMAT_MOODLE, null, null, true);
333
                }
334
            }
335
        }
336
 
337
        if ($changedgrades) {
338
            // If a final grade was overriden reload grades so dependent grades like course total will be correct
339
            $this->grades = null;
340
        }
341
 
342
        return $warnings;
343
    }
344
 
345
 
346
    /**
347
     * Setting the sort order, this depends on last state
348
     * all this should be in the new table class that we might need to use
349
     * for displaying grades.
350
 
351
     * @param string $sort sorting direction
352
     */
353
    private function setup_sortitemid(string $sort = '') {
354
 
355
        global $SESSION;
356
 
357
        if (!isset($SESSION->gradeuserreport)) {
358
            $SESSION->gradeuserreport = new stdClass();
359
        }
360
 
361
        if ($this->sortitemid) {
362
            if (!isset($SESSION->gradeuserreport->sort)) {
363
                $this->sortorder = $SESSION->gradeuserreport->sort = 'ASC';
364
            } else if (!$sort) {
365
                // this is the first sort, i.e. by last name
366
                if (!isset($SESSION->gradeuserreport->sortitemid)) {
367
                    $this->sortorder = $SESSION->gradeuserreport->sort = 'ASC';
368
                } else if ($SESSION->gradeuserreport->sortitemid == $this->sortitemid) {
369
                    // same as last sort
370
                    if ($SESSION->gradeuserreport->sort == 'ASC') {
371
                        $this->sortorder = $SESSION->gradeuserreport->sort = 'DESC';
372
                    } else {
373
                        $this->sortorder = $SESSION->gradeuserreport->sort = 'ASC';
374
                    }
375
                } else {
376
                    $this->sortorder = $SESSION->gradeuserreport->sort = 'ASC';
377
                }
378
            }
379
            $SESSION->gradeuserreport->sortitemid = $this->sortitemid;
380
        } else {
381
            // not requesting sort, use last setting (for paging)
382
 
383
            if (isset($SESSION->gradeuserreport->sortitemid)) {
384
                $this->sortitemid = $SESSION->gradeuserreport->sortitemid;
385
            } else {
386
                $this->sortitemid = 'lastname';
387
            }
388
 
389
            if (isset($SESSION->gradeuserreport->sort)) {
390
                $this->sortorder = $SESSION->gradeuserreport->sort;
391
            } else {
392
                $this->sortorder = 'ASC';
393
            }
394
        }
395
 
396
        // If explicit sorting direction exists.
397
        if ($sort) {
398
            $this->sortorder = $sort;
399
            $SESSION->gradeuserreport->sort = $sort;
400
        }
401
    }
402
 
403
    /**
404
     * pulls out the userids of the users to be display, and sorts them
405
     *
406
     * @param bool $allusers If we are getting the users within the report, we want them all irrespective of paging.
407
     */
408
    public function load_users(bool $allusers = false) {
409
        global $CFG, $DB;
410
 
411
        if (!empty($this->users)) {
412
            return;
413
        }
414
        $this->setup_users();
415
 
416
        // Limit to users with a gradeable role.
417
        list($gradebookrolessql, $gradebookrolesparams) = $DB->get_in_or_equal(explode(',', $this->gradebookroles), SQL_PARAMS_NAMED, 'grbr0');
418
 
419
        // Check the status of showing only active enrolments.
420
        $coursecontext = $this->context->get_course_context(true);
421
        $defaultgradeshowactiveenrol = !empty($CFG->grade_report_showonlyactiveenrol);
422
        $showonlyactiveenrol = get_user_preferences('grade_report_showonlyactiveenrol', $defaultgradeshowactiveenrol);
423
        $showonlyactiveenrol = $showonlyactiveenrol || !has_capability('moodle/course:viewsuspendedusers', $coursecontext);
424
 
425
        // Limit to users with an active enrollment.
426
        list($enrolledsql, $enrolledparams) = get_enrolled_sql($this->context, '', 0, $showonlyactiveenrol);
427
 
428
        // Fields we need from the user table.
429
        $userfieldsapi = \core_user\fields::for_identity($this->context)->with_userpic();
430
        $userfieldssql = $userfieldsapi->get_sql('u', true, '', '', false);
431
 
432
        // We want to query both the current context and parent contexts.
433
        list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($this->context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx');
434
 
435
        // If the user has clicked one of the sort asc/desc arrows.
436
        if (is_numeric($this->sortitemid)) {
437
            $params = array_merge(array('gitemid' => $this->sortitemid), $gradebookrolesparams, $this->userwheresql_params,
438
                    $this->groupwheresql_params, $enrolledparams, $relatedctxparams);
439
 
440
            $sortjoin = "LEFT JOIN {grade_grades} g ON g.userid = u.id AND g.itemid = $this->sortitemid";
441
 
442
            if ($this->sortorder == 'ASC') {
443
                $sort = $DB->sql_order_by_null('g.finalgrade');
444
            } else {
445
                $sort = $DB->sql_order_by_null('g.finalgrade', SORT_DESC);
446
            }
447
            $sort .= ", u.idnumber, u.lastname, u.firstname, u.email";
448
        } else {
449
            $sortjoin = '';
450
 
451
            // The default sort will be that provided by the site for users, unless a valid user field is requested,
452
            // the value of which takes precedence.
453
            [$sort] = users_order_by_sql('u', null, $this->context, $userfieldssql->mappings);
454
            if (array_key_exists($this->sortitemid, $userfieldssql->mappings)) {
455
 
456
                // Ensure user sort field doesn't duplicate one of the default sort fields.
457
                $usersortfield = $userfieldssql->mappings[$this->sortitemid];
458
                $defaultsortfields = array_diff(explode(', ', $sort), [$usersortfield]);
459
 
460
                $sort = "{$usersortfield} {$this->sortorder}, " . implode(', ', $defaultsortfields);
461
            }
462
 
463
            $params = array_merge($gradebookrolesparams, $this->userwheresql_params, $this->groupwheresql_params, $enrolledparams, $relatedctxparams);
464
        }
465
        $params = array_merge($userfieldssql->params, $params);
466
        $sql = "SELECT {$userfieldssql->selects}
467
                  FROM {user} u
468
                        {$userfieldssql->joins}
469
                  JOIN ($enrolledsql) je ON je.id = u.id
470
                       $this->groupsql
471
                       $sortjoin
472
                  JOIN (
473
                           SELECT DISTINCT ra.userid
474
                             FROM {role_assignments} ra
475
                            WHERE ra.roleid IN ($this->gradebookroles)
476
                              AND ra.contextid $relatedctxsql
477
                       ) rainner ON rainner.userid = u.id
478
                   AND u.deleted = 0
479
                   $this->userwheresql
480
                   $this->groupwheresql
481
              ORDER BY $sort";
1441 ariadna 482
        // We never work with unlimited result. Limit the number of records by $this->get_max_students_per_page() if no other limit
483
        // is specified.
1 efrain 484
        $studentsperpage = ($this->get_students_per_page() && !$allusers) ?
1441 ariadna 485
            $this->get_students_per_page() : $this->get_max_students_per_page();
1 efrain 486
        $this->users = $DB->get_records_sql($sql, $params, $studentsperpage * $this->page, $studentsperpage);
487
 
488
        if (empty($this->users)) {
489
            $this->userselect = '';
490
            $this->users = array();
491
            $this->userselectparams = array();
492
        } else {
493
            list($usql, $uparams) = $DB->get_in_or_equal(array_keys($this->users), SQL_PARAMS_NAMED, 'usid0');
494
            $this->userselect = "AND g.userid $usql";
495
            $this->userselectparams = $uparams;
496
 
497
            // First flag everyone as not suspended.
498
            foreach ($this->users as $user) {
499
                $this->users[$user->id]->suspendedenrolment = false;
500
            }
501
 
502
            // If we want to mix both suspended and not suspended users, let's find out who is suspended.
503
            if (!$showonlyactiveenrol) {
504
                $sql = "SELECT ue.userid
505
                          FROM {user_enrolments} ue
506
                          JOIN {enrol} e ON e.id = ue.enrolid
507
                         WHERE ue.userid $usql
508
                               AND ue.status = :uestatus
509
                               AND e.status = :estatus
510
                               AND e.courseid = :courseid
511
                               AND ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2)
512
                      GROUP BY ue.userid";
513
 
514
                $time = time();
515
                $params = array_merge($uparams, array('estatus' => ENROL_INSTANCE_ENABLED, 'uestatus' => ENROL_USER_ACTIVE,
516
                        'courseid' => $coursecontext->instanceid, 'now1' => $time, 'now2' => $time));
517
                $useractiveenrolments = $DB->get_records_sql($sql, $params);
518
 
519
                foreach ($this->users as $user) {
520
                    $this->users[$user->id]->suspendedenrolment = !array_key_exists($user->id, $useractiveenrolments);
521
                }
522
            }
523
        }
524
        return $this->users;
525
    }
526
 
527
    /**
528
     * Load all grade items.
529
     */
530
    protected function get_allgradeitems() {
531
        if (!empty($this->allgradeitems)) {
532
            return $this->allgradeitems;
533
        }
534
        $allgradeitems = grade_item::fetch_all(array('courseid' => $this->courseid));
535
        // But hang on - don't include ones which are set to not show the grade at all.
536
        $this->allgradeitems = array_filter($allgradeitems, function($item) {
537
            return $item->gradetype != GRADE_TYPE_NONE;
538
        });
539
 
540
        return $this->allgradeitems;
541
    }
542
 
543
    /**
1441 ariadna 544
     * Return the maximum number of students we can display per page.
545
     *
546
     * This is based on the number of grade items on the course, to limit the overall number of grades displayed on a single page.
547
     * Trying to display too many grades causes browser issues.
548
     *
549
     * @return int
550
     */
551
    public function get_max_students_per_page(): int {
552
        global $CFG;
553
 
554
        $gradeitemcount = count($this->get_allgradeitems());
555
 
556
        if (isset($CFG->maxgradesperpage) && clean_param($CFG->maxgradesperpage, PARAM_INT) > 0) {
557
            $maxgradesperpage = $CFG->maxgradesperpage;
558
        } else {
559
            $maxgradesperpage = self::MAX_GRADES_PER_PAGE;
560
        }
561
 
562
        if ($gradeitemcount > 0) {
563
            return round($maxgradesperpage / $gradeitemcount);
564
        } else {
565
            return $maxgradesperpage;
566
        }
567
    }
568
 
569
    /**
1 efrain 570
     * we supply the userids in this query, and get all the grades
571
     * pulls out all the grades, this does not need to worry about paging
572
     */
573
    public function load_final_grades() {
574
        global $CFG, $DB;
575
 
576
        if (!empty($this->grades)) {
577
            return;
578
        }
579
 
580
        if (empty($this->users)) {
581
            return;
582
        }
583
 
584
        // please note that we must fetch all grade_grades fields if we want to construct grade_grade object from it!
585
        $params = array_merge(array('courseid'=>$this->courseid), $this->userselectparams);
586
        $sql = "SELECT g.*
587
                  FROM {grade_items} gi,
588
                       {grade_grades} g
589
                 WHERE g.itemid = gi.id AND gi.courseid = :courseid {$this->userselect}";
590
 
591
        $userids = array_keys($this->users);
592
        $allgradeitems = $this->get_allgradeitems();
593
 
594
        if ($grades = $DB->get_records_sql($sql, $params)) {
595
            foreach ($grades as $graderec) {
596
                $grade = new grade_grade($graderec, false);
597
                if (!empty($allgradeitems[$graderec->itemid])) {
598
                    // Note: Filter out grades which have a grade type of GRADE_TYPE_NONE.
599
                    // Only grades without this type are present in $allgradeitems.
600
                    $this->allgrades[$graderec->userid][$graderec->itemid] = $grade;
601
                }
602
                if (in_array($graderec->userid, $userids) and array_key_exists($graderec->itemid, $this->gtree->get_items())) { // some items may not be present!!
603
                    $this->grades[$graderec->userid][$graderec->itemid] = $grade;
604
                    $this->grades[$graderec->userid][$graderec->itemid]->grade_item = $this->gtree->get_item($graderec->itemid); // db caching
605
                }
606
            }
607
        }
608
 
609
        // prefil grades that do not exist yet
610
        foreach ($userids as $userid) {
611
            foreach ($this->gtree->get_items() as $itemid => $unused) {
612
                if (!isset($this->grades[$userid][$itemid])) {
613
                    $this->grades[$userid][$itemid] = new grade_grade();
614
                    $this->grades[$userid][$itemid]->itemid = $itemid;
615
                    $this->grades[$userid][$itemid]->userid = $userid;
616
                    $this->grades[$userid][$itemid]->grade_item = $this->gtree->get_item($itemid); // db caching
617
 
618
                    $this->allgrades[$userid][$itemid] = $this->grades[$userid][$itemid];
619
                }
620
            }
621
        }
622
 
623
        // Pre fill grades for any remaining items which might be collapsed.
624
        foreach ($userids as $userid) {
625
            foreach ($allgradeitems as $itemid => $gradeitem) {
626
                if (!isset($this->allgrades[$userid][$itemid])) {
627
                    $this->allgrades[$userid][$itemid] = new grade_grade();
628
                    $this->allgrades[$userid][$itemid]->itemid = $itemid;
629
                    $this->allgrades[$userid][$itemid]->userid = $userid;
630
                    $this->allgrades[$userid][$itemid]->grade_item = $gradeitem;
631
                }
632
            }
633
        }
634
    }
635
 
636
    /**
637
     * Builds and returns the rows that will make up the left part of the grader report
638
     * This consists of student names and icons, links to user reports and id numbers, as well
639
     * as header cells for these columns. It also includes the fillers required for the
640
     * categories displayed on the right side of the report.
641
     * @param boolean $displayaverages whether to display average rows in the table
642
     * @return array Array of html_table_row objects
643
     */
644
    public function get_left_rows($displayaverages) {
645
        global $CFG, $OUTPUT;
646
 
647
        // Course context to determine how the user details should be displayed.
648
        $coursecontext = context_course::instance($this->courseid);
649
 
650
        $rows = [];
651
 
652
        $showuserimage = $this->get_pref('showuserimage');
653
        $viewfullnames = has_capability('moodle/site:viewfullnames', $this->context);
654
 
655
        $extrafields = \core_user\fields::get_identity_fields($this->context);
656
 
657
        $arrows = $this->get_sort_arrows($extrafields);
658
 
659
        $colspan = 1 + count($extrafields);
660
 
661
        $levels = count($this->gtree->levels) - 1;
662
 
663
        $fillercell = new html_table_cell();
664
        $fillercell->header = true;
665
        $fillercell->attributes['scope'] = 'col';
666
        $fillercell->attributes['class'] = 'cell topleft';
667
        $fillercell->text = html_writer::span(get_string('participants'), 'accesshide');
668
        $fillercell->colspan = $colspan;
669
        $fillercell->rowspan = $levels;
670
        $row = new html_table_row(array($fillercell));
1441 ariadna 671
        if ($levels >= 1) { // Do not display the filler cell if there are no levels as there will be nothing else in the row.
672
            $rows[] = $row;
673
        }
1 efrain 674
 
675
        for ($i = 1; $i < $levels; $i++) {
676
            $row = new html_table_row();
677
            $rows[] = $row;
678
        }
679
 
680
        $headerrow = new html_table_row();
681
        $headerrow->attributes['class'] = 'heading';
682
 
683
        $studentheader = new html_table_cell();
684
        // The browser's scrollbar may partly cover (in certain operative systems) the content in the student header
685
        // when horizontally scrolling through the table contents (most noticeable when in RTL mode).
686
        // Therefore, add slight padding on the left or right when using RTL mode.
1441 ariadna 687
        $studentheader->attributes['class'] = "header ps-3";
1 efrain 688
        $studentheader->scope = 'col';
689
        $studentheader->header = true;
690
        $studentheader->id = 'studentheader';
691
        $element = ['type' => 'userfield', 'name' => 'fullname'];
692
        $studentheader->text = $arrows['studentname'] .
693
            $this->gtree->get_cell_action_menu($element, 'gradeitem', $this->gpr, $this->baseurl);
694
        $headerrow->cells[] = $studentheader;
695
 
696
        foreach ($extrafields as $field) {
697
            $fieldheader = new html_table_cell();
698
            $fieldheader->attributes['class'] = 'userfield user' . $field;
699
            $fieldheader->attributes['data-col'] = $field;
700
            $fieldheader->scope = 'col';
701
            $fieldheader->header = true;
702
 
703
            $collapsecontext = [
704
                'field' => $field,
705
                'name' => \core_user\fields::get_display_name($field),
706
            ];
707
 
708
            $collapsedicon = $OUTPUT->render_from_template('gradereport_grader/collapse/icon', $collapsecontext);
709
            // Need to wrap the button into a div with our hooking element for user items, gradeitems already have this.
710
            $collapsedicon = html_writer::div($collapsedicon, 'd-none', ['data-collapse' => 'expandbutton']);
711
 
712
            $element = ['type' => 'userfield', 'name' => $field];
713
            $fieldheader->text = $arrows[$field] .
714
                $this->gtree->get_cell_action_menu($element, 'gradeitem', $this->gpr, $this->baseurl) . $collapsedicon;
715
            $headerrow->cells[] = $fieldheader;
716
        }
717
 
718
        $rows[] = $headerrow;
719
 
720
        $suspendedstring = null;
721
 
722
        $usercount = 0;
723
        foreach ($this->users as $userid => $user) {
724
            $userrow = new html_table_row();
725
            $userrow->id = 'fixed_user_'.$userid;
726
            $userrow->attributes['class'] = ($usercount % 2) ? 'userrow even' : 'userrow odd';
727
 
728
            $usercell = new html_table_cell();
729
            $usercell->attributes['class'] = ($usercount % 2) ? 'header user even' : 'header user odd';
730
            $usercount++;
731
 
732
            $usercell->header = true;
733
            $usercell->scope = 'row';
734
 
735
            if ($showuserimage) {
736
                $usercell->text = $OUTPUT->render(\core_user::get_profile_picture($user, $coursecontext, [
737
                    'link' => false, 'visibletoscreenreaders' => false
738
                ]));
739
            }
740
 
741
            $fullname = fullname($user, $viewfullnames);
742
            $usercell->text = html_writer::link(
743
                \core_user::get_profile_url($user, $coursecontext),
744
                $usercell->text . $fullname,
745
                ['class' => 'username']
746
            );
747
 
748
            if (!empty($user->suspendedenrolment)) {
749
                $usercell->attributes['class'] .= ' usersuspended';
750
 
751
                //may be lots of suspended users so only get the string once
752
                if (empty($suspendedstring)) {
753
                    $suspendedstring = get_string('userenrolmentsuspended', 'grades');
754
                }
755
                $icon = $OUTPUT->pix_icon('i/enrolmentsuspended', $suspendedstring);
756
                $usercell->text .= html_writer::tag('span', $icon, array('class'=>'usersuspendedicon'));
757
            }
758
            // The browser's scrollbar may partly cover (in certain operative systems) the content in the user cells
759
            // when horizontally scrolling through the table contents (most noticeable when in RTL mode).
760
            // Therefore, add slight padding on the left or right when using RTL mode.
1441 ariadna 761
            $usercell->attributes['class'] .= ' ps-3';
1 efrain 762
            $usercell->text .= $this->gtree->get_cell_action_menu(['userid' => $userid], 'user', $this->gpr);
763
 
764
            $userrow->cells[] = $usercell;
765
 
766
            foreach ($extrafields as $field) {
767
                $fieldcellcontent = s($user->$field);
768
                if ($field === 'country') {
769
                    $countries = get_string_manager()->get_list_of_countries();
770
                    $fieldcellcontent = $countries[$user->$field] ?? $fieldcellcontent;
771
                }
772
 
773
                $fieldcell = new html_table_cell();
774
                $fieldcell->attributes['class'] = 'userfield user' . $field;
775
                $fieldcell->attributes['data-col'] = $field;
776
                $fieldcell->header = false;
777
                $fieldcell->text = html_writer::tag('div', $fieldcellcontent, [
778
                    'data-collapse' => 'content'
779
                ]);
780
 
781
                $userrow->cells[] = $fieldcell;
782
            }
783
 
784
            $userrow->attributes['data-uid'] = $userid;
785
            $rows[] = $userrow;
786
        }
787
 
788
        $rows = $this->get_left_range_row($rows, $colspan);
789
        if ($displayaverages) {
790
            $rows = $this->get_left_avg_row($rows, $colspan, true);
791
            $rows = $this->get_left_avg_row($rows, $colspan);
792
        }
793
 
794
        return $rows;
795
    }
796
 
797
    /**
798
     * Builds and returns the rows that will make up the right part of the grader report
799
     * @param boolean $displayaverages whether to display average rows in the table
800
     * @return array Array of html_table_row objects
801
     */
802
    public function get_right_rows(bool $displayaverages): array {
803
        global $CFG, $USER, $OUTPUT, $DB, $PAGE;
804
 
805
        $rows = [];
806
        $this->rowcount = 0;
807
        $strgrade = get_string('gradenoun');
808
        $this->get_sort_arrows();
809
 
810
        // Get preferences once.
811
        $quickgrading = $this->get_pref('quickgrading');
812
 
813
        // Get strings which are re-used inside the loop.
814
        $strftimedatetimeshort = get_string('strftimedatetimeshort');
815
        $strerror = get_string('error');
816
        $stroverridengrade = get_string('overridden', 'grades');
817
        $strfail = get_string('fail', 'grades');
818
        $strpass = get_string('pass', 'grades');
819
        $viewfullnames = has_capability('moodle/site:viewfullnames', $this->context);
820
 
821
        // Preload scale objects for items with a scaleid and initialize tab indices.
822
        $scaleslist = [];
823
 
824
        foreach ($this->gtree->get_items() as $itemid => $item) {
825
            if (!empty($item->scaleid)) {
826
                $scaleslist[] = $item->scaleid;
827
            }
828
        }
829
 
830
        $cache = \cache::make_from_params(\cache_store::MODE_REQUEST, 'gradereport_grader', 'scales');
831
        $scalesarray = $cache->get(get_class($this));
832
        if (!$scalesarray) {
833
            $scalesarray = $DB->get_records_list('scale', 'id', $scaleslist);
834
            // Save to cache.
835
            $cache->set(get_class($this), $scalesarray);
836
        }
837
 
838
        foreach ($this->gtree->get_levels() as $row) {
839
            $headingrow = new html_table_row();
840
            $headingrow->attributes['class'] = 'heading_name_row';
841
 
842
            foreach ($row as $element) {
843
                $sortlink = clone($this->baseurl);
844
                if (isset($element['object']->id)) {
845
                    $sortlink->param('sortitemid', $element['object']->id);
846
                }
847
 
848
                $type   = $element['type'];
849
 
850
                if (!empty($element['colspan'])) {
851
                    $colspan = $element['colspan'];
852
                } else {
853
                    $colspan = 1;
854
                }
855
 
856
                if (!empty($element['depth'])) {
857
                    $catlevel = 'catlevel'.$element['depth'];
858
                } else {
859
                    $catlevel = '';
860
                }
861
 
862
                // Element is a filler.
863
                if ($type == 'filler' || $type == 'fillerfirst' || $type == 'fillerlast') {
864
                    $fillercell = new html_table_cell();
865
                    $fillercell->attributes['class'] = $type . ' ' . $catlevel;
866
                    $fillercell->colspan = $colspan;
867
                    $fillercell->text = '&nbsp;';
868
 
869
                    // This is a filler cell; don't use a <th>, it'll confuse screen readers.
870
                    $fillercell->header = false;
871
                    $headingrow->cells[] = $fillercell;
872
                } else if ($type == 'category') {
873
                    // Make sure the grade category has a grade total or at least has child grade items.
874
                    if (grade_tree::can_output_item($element)) {
875
                        // Element is a category.
876
                        $categorycell = new html_table_cell();
877
                        $categorycell->attributes['class'] = 'category ' . $catlevel;
878
                        $categorycell->colspan = $colspan;
879
                        $categorycell->header = true;
880
                        $categorycell->scope = 'col';
881
 
882
                        $statusicons = $this->gtree->set_grade_status_icons($element);
883
                        if ($statusicons) {
884
                            $categorycell->attributes['class'] .= ' statusicons';
885
                        }
886
 
887
                        $context = new stdClass();
888
                        $context->courseheader = $this->get_course_header($element);
889
                        $context->actionmenu = $this->gtree->get_cell_action_menu($element, 'gradeitem', $this->gpr);
890
                        $context->statusicons = $statusicons;
891
                        $categorycell->text = $OUTPUT->render_from_template('gradereport_grader/categorycell', $context);
892
 
893
                        $headingrow->cells[] = $categorycell;
894
                    }
895
                } else {
896
                    // Element is a grade_item.
897
 
898
                    $arrow = '';
899
                    if ($element['object']->id == $this->sortitemid) {
900
                        if ($this->sortorder == 'ASC') {
901
                            $arrow = $this->get_sort_arrow('down', $sortlink);
902
                        } else {
903
                            $arrow = $this->get_sort_arrow('up', $sortlink);
904
                        }
905
                    }
906
 
907
                    $collapsecontext = [
908
                        'field' => $element['object']->id,
909
                        'name' => $element['object']->get_name(),
910
                    ];
911
                    $collapsedicon = '';
912
                    // We do not want grade category total items to be hidden away as it is controlled by something else.
913
                    if (!$element['object']->is_aggregate_item()) {
914
                        $collapsedicon = $OUTPUT->render_from_template('gradereport_grader/collapse/icon', $collapsecontext);
915
                    }
916
                    $headerlink = grade_helper::get_element_header($element, true,
917
                        true, false, false, true);
918
 
919
                    $itemcell = new html_table_cell();
920
                    $itemcell->attributes['class'] = $type . ' ' . $catlevel .
921
                        ' highlightable'. ' i'. $element['object']->id;
922
                    $itemcell->attributes['data-itemid'] = $element['object']->id;
923
 
924
                    $singleview = $this->gtree->get_cell_action_menu($element, 'gradeitem', $this->gpr, $this->baseurl);
925
                    $statusicons = $this->gtree->set_grade_status_icons($element);
926
                    if ($statusicons) {
927
                        $itemcell->attributes['class'] .= ' statusicons';
928
                    }
929
 
930
                    $itemcell->attributes['class'] .= $this->get_cell_display_class($element['object']);
931
 
932
                    $itemcell->colspan = $colspan;
933
                    $itemcell->header = true;
934
                    $itemcell->scope = 'col';
935
 
936
                    $context = new stdClass();
937
                    $context->headerlink = $headerlink;
938
                    $context->arrow = $arrow;
939
                    $context->singleview = $singleview;
940
                    $context->statusicons = $statusicons;
941
                    $context->collapsedicon = $collapsedicon;
942
 
943
                    $itemcell->text = $OUTPUT->render_from_template('gradereport_grader/headercell', $context);
944
 
945
                    $headingrow->cells[] = $itemcell;
946
                }
947
            }
948
            $rows[] = $headingrow;
949
        }
950
 
951
        // Get all the grade items if the user can not view hidden grade items.
952
        // It is possible that the user is simply viewing the 'Course total' by switching to the 'Aggregates only' view
953
        // and that this user does not have the ability to view hidden items. In this case we still need to pass all the
954
        // grade items (in case one has been hidden) as the course total shown needs to be adjusted for this particular
955
        // user.
956
        if (!$this->canviewhidden) {
957
            $allgradeitems = $this->get_allgradeitems();
958
        }
959
 
960
        foreach ($this->users as $userid => $user) {
961
 
962
            if ($this->canviewhidden) {
963
                $altered = [];
964
                $unknown = [];
965
            } else {
966
                $usergrades = $this->allgrades[$userid];
967
                $hidingaffected = grade_grade::get_hiding_affected($usergrades, $allgradeitems);
968
                $altered = $hidingaffected['altered'];
969
                $unknown = $hidingaffected['unknowngrades'];
970
                unset($hidingaffected);
971
            }
972
 
973
            $itemrow = new html_table_row();
974
            $itemrow->id = 'user_'.$userid;
975
 
976
            $fullname = fullname($user, $viewfullnames);
977
 
978
            foreach ($this->gtree->items as $itemid => $unused) {
979
                $item =& $this->gtree->items[$itemid];
980
                $grade = $this->grades[$userid][$item->id];
981
 
982
                $itemcell = new html_table_cell();
983
 
984
                $itemcell->id = 'u' . $userid . 'i' . $itemid;
985
                $itemcell->attributes['data-itemid'] = $itemid;
986
                $itemcell->attributes['class'] = 'gradecell';
987
 
988
                // Get the decimal points preference for this item.
989
                $decimalpoints = $item->get_decimals();
990
 
991
                if (array_key_exists($itemid, $unknown)) {
992
                    $gradeval = null;
993
                } else if (array_key_exists($itemid, $altered)) {
994
                    $gradeval = $altered[$itemid];
995
                } else {
996
                    $gradeval = $grade->finalgrade;
997
                }
998
 
999
                $context = new stdClass();
1000
 
1001
                // MDL-11274: Hide grades in the grader report if the current grader
1002
                // doesn't have 'moodle/grade:viewhidden'.
1003
                if (!$this->canviewhidden && $grade->is_hidden()) {
1004
                    if (!empty($CFG->grade_hiddenasdate) && $grade->get_datesubmitted()
1005
                            && !$item->is_category_item() && !$item->is_course_item()) {
1006
                        // The problem here is that we do not have the time when grade value was modified,
1007
                        // 'timemodified' is general modification date for grade_grades records.
1008
                        $context->text = userdate($grade->get_datesubmitted(), $strftimedatetimeshort);
1009
                        $context->extraclasses = 'datesubmitted';
1010
                    } else {
1011
                        $context->text = '-';
1012
                    }
1013
                    $itemcell->text = $OUTPUT->render_from_template('gradereport_grader/cell', $context);
1014
                    $itemrow->cells[] = $itemcell;
1015
                    continue;
1016
                }
1017
 
1018
                // Emulate grade element.
1019
                $eid = $this->gtree->get_grade_eid($grade);
1020
                $element = ['eid' => $eid, 'object' => $grade, 'type' => 'grade'];
1021
 
1022
                $itemcell->attributes['class'] .= ' grade i' . $itemid;
1023
                if ($item->is_category_item()) {
1024
                    $itemcell->attributes['class'] .= ' cat';
1025
                }
1026
                if ($item->is_course_item()) {
1027
                    $itemcell->attributes['class'] .= ' course';
1028
                }
1029
                if ($grade->is_overridden()) {
1030
                    $itemcell->attributes['class'] .= ' overridden';
1031
                    $itemcell->attributes['aria-label'] = $stroverridengrade;
1032
                }
1033
 
1034
                $hidden = '';
1035
                if ($grade->is_hidden()) {
1036
                    $hidden = ' dimmed_text ';
1037
                }
1038
                $gradepass = ' gradefail ';
1039
                $context->gradepassicon = $OUTPUT->pix_icon('i/invalid', $strfail);
1040
                if ($grade->is_passed($item)) {
1041
                    $gradepass = ' gradepass ';
1042
                    $context->gradepassicon = $OUTPUT->pix_icon('i/valid', $strpass);
1043
                } else if (is_null($grade->is_passed($item))) {
1044
                    $gradepass = '';
1045
                    $context->gradepassicon = '';
1046
                }
1047
                $context->statusicons = $this->gtree->set_grade_status_icons($element);
1048
 
1049
                // If in editing mode, we need to print either a text box or a drop down (for scales)
1050
                // grades in item of type grade category or course are not directly editable.
1051
                if ($item->needsupdate) {
1052
                    $context->text = $strerror;
1053
                    $context->extraclasses = 'gradingerror';
1054
                } else if (!empty($USER->editing)) {
1055
                    if ($item->scaleid && !empty($scalesarray[$item->scaleid])) {
1056
                        $itemcell->attributes['class'] .= ' grade_type_scale';
1057
                    } else if ($item->gradetype == GRADE_TYPE_VALUE) {
1058
                        $itemcell->attributes['class'] .= ' grade_type_value';
1059
                    } else if ($item->gradetype == GRADE_TYPE_TEXT) {
1060
                        $itemcell->attributes['class'] .= ' grade_type_text';
1061
                    }
1062
 
1063
                    if ($grade->is_locked()) {
1064
                        $itemcell->attributes['class'] .= ' locked';
1065
                    }
1066
 
1067
                    if ($item->scaleid && !empty($scalesarray[$item->scaleid])) {
1068
                        $context->scale = true;
1069
 
1070
                        $scale = $scalesarray[$item->scaleid];
1071
                        $gradeval = (int)$gradeval; // Scales use only integers.
1072
                        $scales = explode(",", $scale->scale);
1073
                        // Reindex because scale is off 1.
1074
 
1075
                        // MDL-12104 some previous scales might have taken up part of the array
1076
                        // so this needs to be reset.
1077
                        $scaleopt = [];
1078
                        $i = 0;
1079
                        foreach ($scales as $scaleoption) {
1080
                            $i++;
1081
                            $scaleopt[$i] = $scaleoption;
1082
                        }
1083
 
1084
                        if ($quickgrading && $grade->is_editable()) {
1085
                            $context->iseditable = true;
1086
                            if (empty($item->outcomeid)) {
1087
                                $nogradestr = get_string('nograde');
1088
                            } else {
1089
                                $nogradestr = get_string('nooutcome', 'grades');
1090
                            }
1091
                            $attributes = [
1092
                                'id' => 'grade_' . $userid . '_' . $item->id
1093
                            ];
1094
                            $gradelabel = $fullname . ' ' . $item->get_name(true);
1095
 
1096
                            if ($context->statusicons) {
1097
                                $attributes['class'] = 'statusicons';
1098
                            }
1099
 
1100
                            $context->label = html_writer::label(
1101
                                get_string('useractivitygrade', 'gradereport_grader', $gradelabel),
1102
                                $attributes['id'], false, ['class' => 'accesshide']);
1103
                            $context->select = html_writer::select($scaleopt, 'grade['.$userid.']['.$item->id.']',
1104
                                $gradeval, [-1 => $nogradestr], $attributes);
1105
                        } else if (!empty($scale)) {
1106
                            $scales = explode(",", $scale->scale);
1107
 
1108
                            $context->extraclasses = 'gradevalue' . $hidden . $gradepass;
1109
                            // Invalid grade if gradeval < 1.
1110
                            if ($gradeval < 1) {
1111
                                $context->text = '-';
1112
                            } else {
1113
                                // Just in case somebody changes scale.
1114
                                $gradeval = $grade->grade_item->bounded_grade($gradeval);
1115
                                $context->text = $scales[$gradeval - 1];
1116
                            }
1117
                        }
1118
 
1119
                    } else if ($item->gradetype != GRADE_TYPE_TEXT) {
1120
                        // Value type.
1121
                        if ($quickgrading and $grade->is_editable()) {
1122
                            $context->iseditable = true;
1123
 
1124
                            // Set this input field with type="number" if the decimal separator for current language is set to
1125
                            // a period. Other decimal separators may not be recognised by browsers yet which may cause issues
1126
                            // when entering grades.
1127
                            $decsep = get_string('decsep', 'core_langconfig');
1128
                            $context->isnumeric = $decsep === '.';
1129
                            // If we're rendering this as a number field, set min/max attributes, if applicable.
1130
                            if ($context->isnumeric) {
1131
                                $context->minvalue = $item->grademin ?? null;
1132
                                if (empty($CFG->unlimitedgrades)) {
1133
                                    $context->maxvalue = $item->grademax ?? null;
1134
                                }
1135
                            }
1136
 
1137
                            $value = format_float($gradeval, $decimalpoints);
1138
                            $gradelabel = $fullname . ' ' . $item->get_name(true);
1139
 
1140
                            $context->id = 'grade_' . $userid . '_' . $item->id;
1141
                            $context->name = 'grade[' . $userid . '][' . $item->id .']';
1142
                            $context->value = $value;
1143
                            $context->label = get_string('useractivitygrade', 'gradereport_grader', $gradelabel);
1144
                            $context->title = $strgrade;
1145
                            $context->extraclasses = 'form-control';
1146
                            if ($context->statusicons) {
1147
                                $context->extraclasses .= ' statusicons';
1148
                            }
1149
                        } else {
1150
                            $context->extraclasses = 'gradevalue' . $hidden . $gradepass;
1151
                            $context->text = format_float($gradeval, $decimalpoints);
1152
                        }
1153
                    }
1154
 
1155
                } else {
1156
                    // Not editing.
1157
                    $gradedisplaytype = $item->get_displaytype();
1158
 
1159
                    // Letter grades, scales and text grades are left aligned.
1160
                    $textgrade = false;
1161
                    $textgrades = [GRADE_DISPLAY_TYPE_LETTER,
1162
                        GRADE_DISPLAY_TYPE_REAL_LETTER,
1163
                        GRADE_DISPLAY_TYPE_LETTER_REAL,
1164
                        GRADE_DISPLAY_TYPE_LETTER_PERCENTAGE,
1165
                        GRADE_DISPLAY_TYPE_PERCENTAGE_LETTER];
1166
                    if (in_array($gradedisplaytype, $textgrades)) {
1167
                        $textgrade = true;
1168
                    }
1169
 
1170
                    if ($textgrade || ($item->gradetype == GRADE_TYPE_TEXT)) {
1171
                        $itemcell->attributes['class'] .= ' grade_type_text';
1172
                    } else if ($item->scaleid && !empty($scalesarray[$item->scaleid])) {
1173
                        if ($gradedisplaytype == GRADE_DISPLAY_TYPE_PERCENTAGE) {
1174
                            $itemcell->attributes['class'] .= ' grade_type_value';
1175
                        } else {
1176
                            $itemcell->attributes['class'] .= ' grade_type_scale';
1177
                        }
1178
                    } else {
1179
                        $itemcell->attributes['class'] .= ' grade_type_value';
1180
                    }
1181
 
1182
                    if ($item->needsupdate) {
1183
                        $context->text = $strerror;
1184
                        $context->extraclasses = 'gradingerror' . $hidden . $gradepass;
1185
                    } else {
1186
                        // The max and min for an aggregation may be different to the grade_item.
1187
                        if (!is_null($gradeval)) {
1188
                            $item->grademax = $grade->get_grade_max();
1189
                            $item->grademin = $grade->get_grade_min();
1190
                        }
1191
 
1192
                        $context->extraclasses = 'gradevalue ' . $hidden . $gradepass;
1193
                        $context->text = grade_format_gradevalue($gradeval, $item, true,
1194
                            $gradedisplaytype, null);
1441 ariadna 1195
                        $context->text .= penalty_manager::show_penalty_indicator($grade);
1 efrain 1196
                    }
1197
                }
1198
 
1199
                if ($item->gradetype == GRADE_TYPE_TEXT && !empty($grade->feedback)) {
1200
                    $context->text = html_writer::span(shorten_text(strip_tags($grade->feedback), 20), '',
1201
                        ['data-action' => 'feedback', 'role' => 'button', 'data-courseid' => $this->courseid]);
1202
                }
1203
 
1204
                if (!$item->needsupdate && !($item->gradetype == GRADE_TYPE_TEXT && empty($USER->editing))) {
1205
                    $context->actionmenu = $this->gtree->get_cell_action_menu($element, 'gradeitem', $this->gpr);
1206
                }
1207
 
1208
                $itemcell->text = $OUTPUT->render_from_template('gradereport_grader/cell', $context);
1209
 
1210
                if (!empty($this->gradeserror[$item->id][$userid])) {
1211
                    $itemcell->text .= $this->gradeserror[$item->id][$userid];
1212
                }
1213
 
1214
                $itemrow->cells[] = $itemcell;
1215
            }
1216
            $rows[] = $itemrow;
1217
        }
1218
 
1219
        if (!empty($USER->editing)) {
1220
            $PAGE->requires->js_call_amd('core_form/changechecker',
1221
                'watchFormById', ['gradereport_grader']);
1222
        }
1223
 
1224
        $rows = $this->get_right_range_row($rows);
1225
        if ($displayaverages && $this->canviewhidden) {
1226
            $showonlyactiveenrol = $this->show_only_active();
1227
 
1228
            if ($this->currentgroup) {
1229
                $ungradedcounts = $this->ungraded_counts(true, true, $showonlyactiveenrol);
1230
                $rows[] = $this->format_averages($ungradedcounts);
1231
            }
1232
 
1233
            $ungradedcounts = $this->ungraded_counts(false, true, $showonlyactiveenrol);
1234
            $rows[] = $this->format_averages($ungradedcounts);
1235
        }
1236
 
1237
        return $rows;
1238
    }
1239
 
1240
    /**
1241
     * Returns a row of grade items averages
1242
     *
1243
     * @param grade_item $gradeitem Grade item.
1244
     * @param array|null $aggr Average value and meancount information.
1245
     * @param bool|null $shownumberofgrades Whether to show number of grades.
1246
     * @return html_table_cell Formatted average cell.
1247
     */
1248
    protected function format_average_cell(grade_item $gradeitem, ?array $aggr = null, ?bool $shownumberofgrades = null): html_table_cell {
1249
        global $OUTPUT;
1250
 
1251
        if ($gradeitem->needsupdate) {
1252
            $avgcell = new html_table_cell();
1253
            $avgcell->attributes['class'] = 'i' . $gradeitem->id;
1254
            $avgcell->text = $OUTPUT->container(get_string('error'), 'gradingerror');
1255
        } else {
1256
            $gradetypeclass = '';
1257
            if ($gradeitem->gradetype == GRADE_TYPE_SCALE) {
1258
                $gradetypeclass = ' grade_type_scale';
1259
            } else if ($gradeitem->gradetype == GRADE_TYPE_VALUE) {
1260
                $gradetypeclass = ' grade_type_value';
1261
            } else if ($gradeitem->gradetype == GRADE_TYPE_TEXT) {
1262
                $gradetypeclass = ' grade_type_text';
1263
            }
1264
 
1265
            if (empty($aggr['average'])) {
1266
                $avgcell = new html_table_cell();
1267
                $avgcell->attributes['class'] = $gradetypeclass . ' i' . $gradeitem->id;
1268
                $avgcell->attributes['data-itemid'] = $gradeitem->id;
1269
                $avgcell->text = html_writer::div('-', '', ['data-collapse' => 'avgrowcell']);
1270
            } else {
1271
                $numberofgrades = '';
1272
                if ($shownumberofgrades) {
1273
                    $numberofgrades = " (" . $aggr['meancount'] . ")";
1274
                }
1275
 
1276
                $avgcell = new html_table_cell();
1277
                $avgcell->attributes['class'] = $gradetypeclass . ' i' . $gradeitem->id;
1278
                $avgcell->attributes['data-itemid'] = $gradeitem->id;
1279
                $avgcell->text = html_writer::div($aggr['average'] . $numberofgrades, '', ['data-collapse' => 'avgrowcell']);
1280
            }
1281
        }
1282
        return $avgcell;
1283
    }
1284
 
1285
    /**
1286
     * Depending on the style of report (fixedstudents vs traditional one-table),
1287
     * arranges the rows of data in one or two tables, and returns the output of
1288
     * these tables in HTML
1289
     * @param boolean $displayaverages whether to display average rows in the table
1290
     * @return string HTML
1291
     */
1292
    public function get_grade_table($displayaverages = false) {
1293
        global $OUTPUT;
1294
        $leftrows = $this->get_left_rows($displayaverages);
1295
        $rightrows = $this->get_right_rows($displayaverages);
1296
 
1297
        $html = '';
1298
 
1299
        $fulltable = new html_table();
1300
        $fulltable->attributes['class'] = 'gradereport-grader-table d-none';
1301
        $fulltable->id = 'user-grades';
1302
        $fulltable->caption = get_string('summarygrader', 'gradereport_grader');
1303
        $fulltable->captionhide = true;
1304
        // We don't want the table to be enclosed within in a .table-responsive div as it is heavily customised.
1305
        $fulltable->responsive = false;
1306
 
1307
        // Extract rows from each side (left and right) and collate them into one row each
1308
        foreach ($leftrows as $key => $row) {
1441 ariadna 1309
            if (isset($rightrows[$key])) {
1310
                $row->cells = array_merge($row->cells, $rightrows[$key]->cells);
1311
                $fulltable->data[] = $row;
1312
                unset($leftrows[$key]);
1313
                unset($rightrows[$key]);
1314
            } else { // Right row is not set - this is the case of the left side.
1315
                $fulltable->data[] = $row;
1316
                unset($leftrows[$key]);
1317
            }
1 efrain 1318
        }
1319
        $html .= html_writer::table($fulltable);
1320
        return $OUTPUT->container($html, 'gradeparent');
1321
    }
1322
 
1323
    /**
1324
     * Builds and return the row of icons for the left side of the report.
1325
     * It only has one cell that says "Controls"
1326
     * @param array $rows The Array of rows for the left part of the report
1327
     * @param int $colspan The number of columns this cell has to span
1328
     * @return array Array of rows for the left part of the report
1329
     * @deprecated since Moodle 4.2 - The row is not shown anymore - we have actions menu.
1330
     * @todo MDL-77307 This will be deleted in Moodle 4.6.
1331
     */
1332
    public function get_left_icons_row($rows=array(), $colspan=1) {
1333
        global $USER;
1334
 
1335
        debugging('The function get_left_icons_row() is deprecated, please do not use it anymore.',
1336
            DEBUG_DEVELOPER);
1337
 
1338
        if (!empty($USER->editing)) {
1339
            $controlsrow = new html_table_row();
1340
            $controlsrow->attributes['class'] = 'controls';
1341
            $controlscell = new html_table_cell();
1342
            $controlscell->attributes['class'] = 'header controls';
1343
            $controlscell->header = true;
1344
            $controlscell->colspan = $colspan;
1345
            $controlscell->text = get_string('controls', 'grades');
1346
            $controlsrow->cells[] = $controlscell;
1347
 
1348
            $rows[] = $controlsrow;
1349
        }
1350
        return $rows;
1351
    }
1352
 
1353
    /**
1354
     * Builds and return the header for the row of ranges, for the left part of the grader report.
1355
     * @param array $rows The Array of rows for the left part of the report
1356
     * @param int $colspan The number of columns this cell has to span
1357
     * @return array Array of rows for the left part of the report
1358
     */
1359
    public function get_left_range_row($rows=array(), $colspan=1) {
1360
        global $CFG, $USER;
1361
 
1362
        if ($this->get_pref('showranges')) {
1363
            $rangerow = new html_table_row();
1364
            $rangerow->attributes['class'] = 'range r'.$this->rowcount++;
1365
            $rangecell = new html_table_cell();
1366
            $rangecell->attributes['class'] = 'header range';
1367
            $rangecell->colspan = $colspan;
1368
            $rangecell->header = true;
1369
            $rangecell->scope = 'row';
1370
            $rangecell->text = get_string('range', 'grades');
1371
            $rangerow->cells[] = $rangecell;
1372
            $rows[] = $rangerow;
1373
        }
1374
 
1375
        return $rows;
1376
    }
1377
 
1378
    /**
1379
     * Builds and return the headers for the rows of averages, for the left part of the grader report.
1380
     * @param array $rows The Array of rows for the left part of the report
1381
     * @param int $colspan The number of columns this cell has to span
1382
     * @param bool $groupavg If true, returns the row for group averages, otherwise for overall averages
1383
     * @return array Array of rows for the left part of the report
1384
     */
1385
    public function get_left_avg_row($rows=array(), $colspan=1, $groupavg=false) {
1386
        if (!$this->canviewhidden) {
1387
            // totals might be affected by hiding, if user can not see hidden grades the aggregations might be altered
1388
            // better not show them at all if user can not see all hideen grades
1389
            return $rows;
1390
        }
1391
 
1392
        $showaverages = $this->get_pref('showaverages');
1393
        $showaveragesgroup = $this->currentgroup && $showaverages;
1394
        $straveragegroup = get_string('groupavg', 'grades');
1395
 
1396
        if ($groupavg) {
1397
            if ($showaveragesgroup) {
1398
                $groupavgrow = new html_table_row();
1399
                $groupavgrow->attributes['class'] = 'groupavg r'.$this->rowcount++;
1400
                $groupavgcell = new html_table_cell();
1401
                $groupavgcell->attributes['class'] = 'header range';
1402
                $groupavgcell->colspan = $colspan;
1403
                $groupavgcell->header = true;
1404
                $groupavgcell->scope = 'row';
1405
                $groupavgcell->text = $straveragegroup;
1406
                $groupavgrow->cells[] = $groupavgcell;
1407
                $rows[] = $groupavgrow;
1408
            }
1409
        } else {
1410
            $straverage = get_string('overallaverage', 'grades');
1411
 
1412
            if ($showaverages) {
1413
                $avgrow = new html_table_row();
1414
                $avgrow->attributes['class'] = 'avg r'.$this->rowcount++;
1415
                $avgcell = new html_table_cell();
1416
                $avgcell->attributes['class'] = 'header range';
1417
                $avgcell->colspan = $colspan;
1418
                $avgcell->header = true;
1419
                $avgcell->scope = 'row';
1420
                $avgcell->text = $straverage;
1421
                $avgrow->cells[] = $avgcell;
1422
                $rows[] = $avgrow;
1423
            }
1424
        }
1425
 
1426
        return $rows;
1427
    }
1428
 
1429
    /**
1430
     * Builds and return the row of icons when editing is on, for the right part of the grader report.
1431
     * @param array $rows The Array of rows for the right part of the report
1432
     * @return array Array of rows for the right part of the report
1433
     * @deprecated since Moodle 4.2 - The row is not shown anymore - we have actions menu.
1434
     * @todo MDL-77307 This will be deleted in Moodle 4.6.
1435
     */
1436
    public function get_right_icons_row($rows=array()) {
1437
        global $USER;
1438
        debugging('The function get_right_icons_row() is deprecated, please do not use it anymore.',
1439
            DEBUG_DEVELOPER);
1440
 
1441
        if (!empty($USER->editing)) {
1442
            $iconsrow = new html_table_row();
1443
            $iconsrow->attributes['class'] = 'controls';
1444
 
1445
            foreach ($this->gtree->items as $itemid => $unused) {
1446
                // emulate grade element
1447
                $item = $this->gtree->get_item($itemid);
1448
 
1449
                $eid = $this->gtree->get_item_eid($item);
1450
                $element = $this->gtree->locate_element($eid);
1451
                $itemcell = new html_table_cell();
1452
                $itemcell->attributes['class'] = 'controls icons i'.$itemid;
1453
                $itemcell->text = $this->get_icons($element);
1454
                $iconsrow->cells[] = $itemcell;
1455
            }
1456
            $rows[] = $iconsrow;
1457
        }
1458
        return $rows;
1459
    }
1460
 
1461
    /**
1462
     * Builds and return the row of ranges for the right part of the grader report.
1463
     * @param array $rows The Array of rows for the right part of the report
1464
     * @return array Array of rows for the right part of the report
1465
     */
1466
    public function get_right_range_row($rows=array()) {
1467
 
1468
        if ($this->get_pref('showranges')) {
1469
            $rangesdisplaytype   = $this->get_pref('rangesdisplaytype');
1470
            $rangesdecimalpoints = $this->get_pref('rangesdecimalpoints');
1471
            $rangerow = new html_table_row();
1472
            $rangerow->attributes['class'] = 'heading range';
1473
 
1474
            foreach ($this->gtree->items as $itemid => $unused) {
1475
                $item =& $this->gtree->items[$itemid];
1476
                $itemcell = new html_table_cell();
1477
                $itemcell->attributes['class'] .= ' range i'. $itemid;
1478
                $itemcell->attributes['class'] .= $this->get_cell_display_class($item);
1479
 
1480
                $hidden = '';
1481
                if ($item->is_hidden()) {
1482
                    $hidden = ' dimmed_text ';
1483
                }
1484
 
1485
                $formattedrange = $item->get_formatted_range($rangesdisplaytype, $rangesdecimalpoints);
1486
 
1487
                $itemcell->attributes['data-itemid'] = $itemid;
1488
                $itemcell->text = html_writer::div($formattedrange, 'rangevalues' . $hidden,
1489
                    ['data-collapse' => 'rangerowcell']);
1490
                $rangerow->cells[] = $itemcell;
1491
            }
1492
            $rows[] = $rangerow;
1493
        }
1494
        return $rows;
1495
    }
1496
 
1497
    /**
1498
     * @deprecated since Moodle 4.4 - Call calculate_average instead.
1499
     */
1441 ariadna 1500
    #[\core\attribute\deprecated('grade_report::calculate_average()', since: '4.4', final: true)]
1501
    public function get_right_avg_row() {
1502
        \core\deprecation::emit_deprecation([self::class, __FUNCTION__]);
1 efrain 1503
    }
1504
 
1505
    /**
1506
     * Given element category, create a collapsible icon and
1507
     * course header.
1508
     *
1509
     * @param array $element
1510
     * @return string HTML
1511
     */
1512
    protected function get_course_header($element) {
1513
        if (in_array($element['object']->id, $this->collapsed['aggregatesonly'])) {
1514
            $showing = get_string('showingaggregatesonly', 'grades');
1515
        } else if (in_array($element['object']->id, $this->collapsed['gradesonly'])) {
1516
            $showing = get_string('showinggradesonly', 'grades');
1517
        } else {
1518
            $showing = get_string('showingfullmode', 'grades');
1519
        }
1520
 
1521
        $name = $element['object']->get_name();
1522
        $nameunescaped = $element['object']->get_name(false);
1523
        $describedbyid = uniqid();
1524
        $courseheader = html_writer::tag('span', $name, [
1525
            'title' => $nameunescaped,
1526
            'class' => 'gradeitemheader',
1527
            'aria-describedby' => $describedbyid
1528
        ]);
1441 ariadna 1529
        $courseheader .= html_writer::div($showing, 'visually-hidden', [
1 efrain 1530
            'id' => $describedbyid
1531
        ]);
1532
 
1533
        return $courseheader;
1534
    }
1535
 
1536
    /**
1537
     * Given a grade_category, grade_item or grade_grade, this function
1538
     * figures out the state of the object and builds then returns a div
1539
     * with the icons needed for the grader report.
1540
     *
1541
     * @param array $element
1542
     * @return string HTML
1543
     * @deprecated since Moodle 4.2 - The row is not shown anymore - we have actions menu.
1544
     * @todo MDL-77307 This will be deleted in Moodle 4.6.
1545
     */
1546
    protected function get_icons($element) {
1547
        global $CFG, $USER, $OUTPUT;
1548
        debugging('The function get_icons() is deprecated, please do not use it anymore.',
1549
            DEBUG_DEVELOPER);
1550
 
1551
        if (empty($USER->editing)) {
1552
            return '<div class="grade_icons" />';
1553
        }
1554
 
1555
        // Init all icons
1556
        $editicon = '';
1557
 
1558
        $editable = true;
1559
 
1560
        if ($element['type'] == 'grade') {
1561
            $item = $element['object']->grade_item;
1562
            if ($item->is_course_item() or $item->is_category_item()) {
1563
                $editable = $this->overridecat;
1564
            }
1565
        }
1566
 
1567
        if ($element['type'] != 'categoryitem' && $element['type'] != 'courseitem' && $editable) {
1568
            $editicon = $this->gtree->get_edit_icon($element, $this->gpr);
1569
        }
1570
 
1571
        $editcalculationicon = '';
1572
        $showhideicon        = '';
1573
        $lockunlockicon      = '';
1574
 
1575
        if (has_capability('moodle/grade:manage', $this->context)) {
1576
            $editcalculationicon = $this->gtree->get_calculation_icon($element, $this->gpr);
1577
 
1578
            $showhideicon = $this->gtree->get_hiding_icon($element, $this->gpr);
1579
 
1580
            $lockunlockicon = $this->gtree->get_locking_icon($element, $this->gpr);
1581
        }
1582
 
1583
        $gradeanalysisicon = '';
1584
        if ($element['type'] == 'grade') {
1585
            $gradeanalysisicon .= $this->gtree->get_grade_analysis_icon($element['object']);
1586
        }
1587
 
1588
        return $OUTPUT->container($editicon.$editcalculationicon.$showhideicon.$lockunlockicon.$gradeanalysisicon, 'grade_icons');
1589
    }
1590
 
1591
    /**
1592
     * Processes a single action against a category, grade_item or grade.
1593
     * @param string $target eid ({type}{id}, e.g. c4 for category4)
1594
     * @param string $action Which action to take (edit, delete etc...)
1595
     * @return
1596
     */
1597
    public function process_action($target, $action) {
1598
        return self::do_process_action($target, $action, $this->course->id);
1599
    }
1600
 
1601
    /**
1602
     * From the list of categories that this user prefers to collapse choose ones that belong to the current course.
1603
     *
1604
     * This function serves two purposes.
1605
     * Mainly it helps migrating from user preference style when all courses were stored in one preference.
1606
     * Also it helps to remove the settings for categories that were removed if the array for one course grows too big.
1607
     *
1608
     * @param int $courseid
1609
     * @param array $collapsed
1610
     * @return array
1611
     */
1612
    protected static function filter_collapsed_categories($courseid, $collapsed) {
1613
        global $DB;
1614
        // Ensure we always have an element for aggregatesonly and another for gradesonly, no matter it's empty.
1615
        $collapsed['aggregatesonly'] = $collapsed['aggregatesonly'] ?? [];
1616
        $collapsed['gradesonly'] = $collapsed['gradesonly'] ?? [];
1617
 
1618
        if (empty($collapsed['aggregatesonly']) && empty($collapsed['gradesonly'])) {
1619
            return $collapsed;
1620
        }
1621
        $cats = $DB->get_fieldset_select('grade_categories', 'id', 'courseid = ?', array($courseid));
1622
        $collapsed['aggregatesonly'] = array_values(array_intersect($collapsed['aggregatesonly'], $cats));
1623
        $collapsed['gradesonly'] = array_values(array_intersect($collapsed['gradesonly'], $cats));
1624
        return $collapsed;
1625
    }
1626
 
1627
    /**
1628
     * Returns the list of categories that this user wants to collapse or display aggregatesonly
1629
     *
1630
     * This method also migrates on request from the old format of storing user preferences when they were stored
1631
     * in one preference for all courses causing DB error when trying to insert very big value.
1632
     *
1633
     * @param int $courseid
1634
     * @return array
1635
     */
1636
    protected static function get_collapsed_preferences($courseid) {
1637
        if ($collapsed = get_user_preferences('grade_report_grader_collapsed_categories'.$courseid)) {
1638
            $collapsed = json_decode($collapsed, true);
1639
            // Ensure we always have an element for aggregatesonly and another for gradesonly, no matter it's empty.
1640
            $collapsed['aggregatesonly'] = $collapsed['aggregatesonly'] ?? [];
1641
            $collapsed['gradesonly'] = $collapsed['gradesonly'] ?? [];
1642
            return $collapsed;
1643
        }
1644
 
1645
        // Try looking for old location of user setting that used to store all courses in one serialized user preference.
1646
        $collapsed = ['aggregatesonly' => [], 'gradesonly' => []]; // Use this if old settings are not found.
1647
        $collapsedall = [];
1648
        $oldprefexists = false;
1649
        if (($oldcollapsedpref = get_user_preferences('grade_report_grader_collapsed_categories')) !== null) {
1650
            $oldprefexists = true;
1651
            if ($collapsedall = unserialize_array($oldcollapsedpref)) {
1652
                // Ensure we always have an element for aggregatesonly and another for gradesonly, no matter it's empty.
1653
                $collapsedall['aggregatesonly'] = $collapsedall['aggregatesonly'] ?? [];
1654
                $collapsedall['gradesonly'] = $collapsedall['gradesonly'] ?? [];
1655
                // We found the old-style preference, filter out only categories that belong to this course and update the prefs.
1656
                $collapsed = static::filter_collapsed_categories($courseid, $collapsedall);
1657
                if (!empty($collapsed['aggregatesonly']) || !empty($collapsed['gradesonly'])) {
1658
                    static::set_collapsed_preferences($courseid, $collapsed);
1659
                    $collapsedall['aggregatesonly'] = array_diff($collapsedall['aggregatesonly'], $collapsed['aggregatesonly']);
1660
                    $collapsedall['gradesonly'] = array_diff($collapsedall['gradesonly'], $collapsed['gradesonly']);
1661
                    if (!empty($collapsedall['aggregatesonly']) || !empty($collapsedall['gradesonly'])) {
1662
                        set_user_preference('grade_report_grader_collapsed_categories', serialize($collapsedall));
1663
                    }
1664
                }
1665
            }
1666
        }
1667
 
1668
        // Arrived here, if the old pref exists and it doesn't contain
1669
        // more information, it means that the migration of all the
1670
        // data to new, by course, preferences is completed, so
1671
        // the old one can be safely deleted.
1672
        if ($oldprefexists &&
1673
                empty($collapsedall['aggregatesonly']) &&
1674
                empty($collapsedall['gradesonly'])) {
1675
            unset_user_preference('grade_report_grader_collapsed_categories');
1676
        }
1677
 
1678
        return $collapsed;
1679
    }
1680
 
1681
    /**
1682
     * Sets the list of categories that user wants to see collapsed in user preferences
1683
     *
1684
     * This method may filter or even trim the list if it does not fit in DB field.
1685
     *
1686
     * @param int $courseid
1687
     * @param array $collapsed
1688
     */
1689
    protected static function set_collapsed_preferences($courseid, $collapsed) {
1690
        global $DB;
1691
        // In an unlikely case that the list of collapsed categories for one course is too big for the user preference size,
1692
        // try to filter the list of categories since array may contain categories that were deleted.
1693
        if (strlen(json_encode($collapsed)) >= 1333) {
1694
            $collapsed = static::filter_collapsed_categories($courseid, $collapsed);
1695
        }
1696
 
1697
        // If this did not help, "forget" about some of the collapsed categories. Still better than to loose all information.
1698
        while (strlen(json_encode($collapsed)) >= 1333) {
1699
            if (count($collapsed['aggregatesonly'])) {
1700
                array_pop($collapsed['aggregatesonly']);
1701
            }
1702
            if (count($collapsed['gradesonly'])) {
1703
                array_pop($collapsed['gradesonly']);
1704
            }
1705
        }
1706
 
1707
        if (!empty($collapsed['aggregatesonly']) || !empty($collapsed['gradesonly'])) {
1708
            set_user_preference('grade_report_grader_collapsed_categories'.$courseid, json_encode($collapsed));
1709
        } else {
1710
            unset_user_preference('grade_report_grader_collapsed_categories'.$courseid);
1711
        }
1712
    }
1713
 
1714
    /**
1715
     * Processes a single action against a category, grade_item or grade.
1716
     * @param string $target eid ({type}{id}, e.g. c4 for category4)
1717
     * @param string $action Which action to take (edit, delete etc...)
1718
     * @param int $courseid affected course.
1719
     * @return
1720
     */
1721
    public static function do_process_action($target, $action, $courseid = null) {
1722
        global $DB;
1723
        // TODO: this code should be in some grade_tree static method
1724
        $targettype = substr($target, 0, 2);
1725
        $targetid = substr($target, 2);
1726
        // TODO: end
1727
 
1728
        if ($targettype !== 'cg') {
1729
            // The following code only works with categories.
1730
            return true;
1731
        }
1732
 
1733
        if (!$courseid) {
1734
            debugging('Function grade_report_grader::do_process_action() now requires additional argument courseid',
1735
                DEBUG_DEVELOPER);
1736
            if (!$courseid = $DB->get_field('grade_categories', 'courseid', array('id' => $targetid), IGNORE_MISSING)) {
1737
                return true;
1738
            }
1739
        }
1740
 
1741
        $collapsed = static::get_collapsed_preferences($courseid);
1742
 
1743
        switch ($action) {
1744
            case 'switch_minus': // Add category to array of aggregatesonly
1745
                $key = array_search($targetid, $collapsed['gradesonly']);
1746
                if ($key !== false) {
1747
                    unset($collapsed['gradesonly'][$key]);
1748
                }
1749
                if (!in_array($targetid, $collapsed['aggregatesonly'])) {
1750
                    $collapsed['aggregatesonly'][] = $targetid;
1751
                    static::set_collapsed_preferences($courseid, $collapsed);
1752
                }
1753
                break;
1754
 
1755
            case 'switch_plus': // Remove category from array of aggregatesonly, and add it to array of gradesonly
1756
                $key = array_search($targetid, $collapsed['aggregatesonly']);
1757
                if ($key !== false) {
1758
                    unset($collapsed['aggregatesonly'][$key]);
1759
                }
1760
                if (!in_array($targetid, $collapsed['gradesonly'])) {
1761
                    $collapsed['gradesonly'][] = $targetid;
1762
                }
1763
                static::set_collapsed_preferences($courseid, $collapsed);
1764
                break;
1765
            case 'switch_whole': // Remove the category from the array of collapsed cats
1766
                $key = array_search($targetid, $collapsed['gradesonly']);
1767
                if ($key !== false) {
1768
                    unset($collapsed['gradesonly'][$key]);
1769
                    static::set_collapsed_preferences($courseid, $collapsed);
1770
                }
1771
 
1772
                $key = array_search($targetid, $collapsed['aggregatesonly']);
1773
                if ($key !== false) {
1774
                    unset($collapsed['aggregatesonly'][$key]);
1775
                    static::set_collapsed_preferences($courseid, $collapsed);
1776
                }
1777
                break;
1778
            default:
1779
                break;
1780
        }
1781
 
1782
        return true;
1783
    }
1784
 
1785
    /**
1786
     * Refactored function for generating HTML of sorting links with matching arrows.
1787
     * Returns an array with 'studentname' and 'idnumber' as keys, with HTML ready
1788
     * to inject into a table header cell.
1789
     * @param array $extrafields Array of extra fields being displayed, such as
1790
     *   user idnumber
1791
     * @return array An associative array of HTML sorting links+arrows
1792
     */
1793
    public function get_sort_arrows(array $extrafields = []) {
1794
        global $CFG;
1795
        $arrows = array();
1796
        $sortlink = clone($this->baseurl);
1797
 
1798
        // Sourced from tablelib.php
1799
        // Check the full name display for sortable fields.
1800
        if (has_capability('moodle/site:viewfullnames', $this->context)) {
1801
            $nameformat = $CFG->alternativefullnameformat;
1802
        } else {
1803
            $nameformat = $CFG->fullnamedisplay;
1804
        }
1805
 
1806
        if ($nameformat == 'language') {
1807
            $nameformat = get_string('fullnamedisplay');
1808
        }
1809
 
1810
        $arrows['studentname'] = '';
1811
        $requirednames = order_in_string(\core_user\fields::get_name_fields(), $nameformat);
1812
        if (!empty($requirednames)) {
1813
            foreach ($requirednames as $name) {
1814
                $arrows['studentname'] .= get_string($name);
1815
                if ($this->sortitemid == $name) {
1816
                    $sortlink->param('sortitemid', $name);
1817
                    if ($this->sortorder == 'ASC') {
1818
                        $sorticon = $this->get_sort_arrow('down', $sortlink);
1819
                    } else {
1820
                        $sorticon = $this->get_sort_arrow('up', $sortlink);
1821
                    }
1822
                    $arrows['studentname'] .= $sorticon;
1823
                }
1824
                $arrows['studentname'] .= ' / ';
1825
            }
1826
 
1827
            $arrows['studentname'] = substr($arrows['studentname'], 0, -3);
1828
        }
1829
 
1830
        foreach ($extrafields as $field) {
1831
            $attributes = [
1832
                'data-collapse' => 'content'
1833
            ];
1834
            // With additional user profile fields, we can't grab the name via WS, so conditionally add it to rip out of the DOM.
1835
            if (preg_match(\core_user\fields::PROFILE_FIELD_REGEX, $field)) {
1836
                $attributes['data-collapse-name'] = \core_user\fields::get_display_name($field);
1837
            }
1838
 
1839
            $arrows[$field] = html_writer::span(\core_user\fields::get_display_name($field), '', $attributes);
1840
            if ($field == $this->sortitemid) {
1841
                $sortlink->param('sortitemid', $field);
1842
 
1843
                if ($this->sortorder == 'ASC') {
1844
                    $sorticon = $this->get_sort_arrow('down', $sortlink);
1845
                } else {
1846
                    $sorticon = $this->get_sort_arrow('up', $sortlink);
1847
                }
1848
                $arrows[$field] .= $sorticon;
1849
            }
1850
        }
1851
 
1852
        return $arrows;
1853
    }
1854
 
1855
    /**
1856
     * Returns the maximum number of students to be displayed on each page
1857
     *
1858
     * @return int The maximum number of students to display per page
1859
     */
1860
    public function get_students_per_page(): int {
1861
        // Default to the lowest available option.
1862
        return (int) get_user_preferences('grade_report_studentsperpage', min(static::PAGINATION_OPTIONS));
1863
    }
1864
 
1865
    /**
1866
     * Returns link to change category view mode.
1867
     *
1868
     * @param moodle_url $url Url to grader report page
1869
     * @param string $title Menu item title
1870
     * @param string $action View mode to change to
1871
     * @param bool $active Whether link is active in dropdown
1872
     *
1873
     * @return string|null
1874
     */
1875
    public function get_category_view_mode_link(moodle_url $url, string $title, string $action, bool $active = false): ?string {
1876
        $urlnew = $url;
1877
        $urlnew->param('action', $action);
1878
        $active = $active ? 'true' : 'false';
1879
        return html_writer::link($urlnew, $title,
1880
            ['class' => 'dropdown-item', 'aria-label' => $title, 'aria-current' => $active, 'role' => 'menuitem']);
1881
    }
1882
 
1883
    /**
1884
     * Return the link to allow the field to collapse from the users view.
1885
     *
1886
     * @return string Dropdown menu link that'll trigger the collapsing functionality.
1887
     * @throws coding_exception
1888
     * @throws moodle_exception
1889
     */
1890
    public function get_hide_show_link(): string {
1891
        $link = new moodle_url('#', []);
1892
        return html_writer::link(
1893
            $link->out(false),
1894
            get_string('collapse'),
1895
            ['class' => 'dropdown-item', 'data-hider' => 'hide', 'aria-label' => get_string('collapse'), 'role' => 'menuitem'],
1896
        );
1897
    }
1898
 
1899
    /**
1900
     * Return the base report link with some default sorting applied.
1901
     *
1902
     * @return string
1903
     * @throws moodle_exception
1904
     */
1905
    public function get_default_sortable(): string {
1906
        $sortlink = new moodle_url('/grade/report/grader/index.php', [
1907
            'id' => $this->courseid,
1908
            'sortitemid' => 'firstname',
1909
            'sort' => 'asc'
1910
        ]);
1911
        $this->gpr->add_url_params($sortlink);
1912
        return $sortlink->out(false);
1913
    }
1914
 
1915
    /**
1916
     * Return class used for text alignment.
1917
     *
1918
     * @param grade_item $item Can be grade item or grade
1919
     * @return string class name used for text alignment
1920
     */
1921
    public function get_cell_display_class(grade_item $item): string {
1922
        global $USER;
1923
 
1924
        $gradetypeclass = '';
1925
        if (!empty($USER->editing)) {
1926
            switch ($item->gradetype) {
1927
                case GRADE_TYPE_SCALE:
1928
                    $gradetypeclass = ' grade_type_scale';
1929
                    break;
1930
                case GRADE_TYPE_VALUE:
1931
                    $gradetypeclass = ' grade_type_value';
1932
                    break;
1933
                case GRADE_TYPE_TEXT:
1934
                    $gradetypeclass = ' grade_type_text';
1935
                    break;
1936
            }
1937
        } else {
1938
            $gradedisplaytype = $item->get_displaytype();
1939
 
1940
            // Letter grades, scales and text grades are left aligned.
1941
            $textgrade = false;
1942
            $textgrades = [GRADE_DISPLAY_TYPE_LETTER,
1943
                GRADE_DISPLAY_TYPE_REAL_LETTER,
1944
                GRADE_DISPLAY_TYPE_LETTER_REAL,
1945
                GRADE_DISPLAY_TYPE_LETTER_PERCENTAGE,
1946
                GRADE_DISPLAY_TYPE_PERCENTAGE_LETTER];
1947
            if (in_array($gradedisplaytype, $textgrades)) {
1948
                $textgrade = true;
1949
            }
1950
 
1951
            $cache = \cache::make_from_params(\cache_store::MODE_REQUEST, 'gradereport_grader', 'scales');
1952
            $scalesarray = $cache->get(get_class($this));
1953
 
1954
            if ($textgrade || ($item->gradetype == GRADE_TYPE_TEXT)) {
1955
                $gradetypeclass = ' grade_type_text';
1956
            } else if ($item->scaleid && !empty($scalesarray[$item->scaleid])) {
1957
                if ($gradedisplaytype == GRADE_DISPLAY_TYPE_PERCENTAGE) {
1958
                    $gradetypeclass = ' grade_type_value';
1959
                } else {
1960
                    $gradetypeclass = ' grade_type_scale';
1961
                }
1962
            } else {
1963
                $gradetypeclass = ' grade_type_value';
1964
            }
1965
        }
1966
        return $gradetypeclass;
1967
    }
1968
}
1969
 
1970
/**
1971
 * Adds report specific context variable
1972
 *
1973
 * @param context_course $context Course context
1974
 * @param int $courseid Course ID
1975
 * @param array  $element An array representing an element in the grade_tree
1976
 * @param grade_plugin_return $gpr A grade_plugin_return object
1977
 * @param string $mode Not used
1978
 * @param stdClass|null $templatecontext Template context
1979
 * @return stdClass|null
1980
 */
1981
function gradereport_grader_get_report_link(context_course $context, int $courseid,
1982
        array $element, grade_plugin_return $gpr, string $mode, ?stdClass $templatecontext): ?stdClass {
1983
 
1984
    static $report = null;
1985
    if (!$report) {
1986
        $report = new grade_report_grader($courseid, $gpr, $context);
1987
    }
1988
 
1989
    if ($mode == 'category') {
1990
        if (!isset($templatecontext)) {
1991
            $templatecontext = new stdClass();
1992
        }
1993
 
1994
        $categoryid = $element['object']->id;
1995
 
1996
        // Load language strings.
1997
        $strswitchminus = get_string('aggregatesonly', 'grades');
1998
        $strswitchplus = get_string('gradesonly', 'grades');
1999
        $strswitchwhole = get_string('fullmode', 'grades');
2000
 
2001
        $url = new moodle_url($gpr->get_return_url(null, ['target' => $element['eid'], 'sesskey' => sesskey()]));
2002
 
2003
        $gradesonly = false;
2004
        $aggregatesonly = false;
2005
        $fullmode = false;
2006
        if (in_array($categoryid, $report->collapsed['gradesonly'])) {
2007
            $gradesonly = true;
2008
        } else if (in_array($categoryid, $report->collapsed['aggregatesonly'])) {
2009
            $aggregatesonly = true;
2010
        } else {
2011
            $fullmode = true;
2012
        }
2013
        $templatecontext->gradesonlyurl =
2014
            $report->get_category_view_mode_link($url, $strswitchplus, 'switch_plus', $gradesonly);
2015
        $templatecontext->aggregatesonlyurl =
2016
            $report->get_category_view_mode_link($url, $strswitchminus, 'switch_minus', $aggregatesonly);
2017
        $templatecontext->fullmodeurl =
2018
            $report->get_category_view_mode_link($url, $strswitchwhole, 'switch_whole', $fullmode);
2019
        return $templatecontext;
2020
    } else if ($mode == 'gradeitem') {
2021
        if (($element['type'] == 'userfield') && ($element['name'] !== 'fullname')) {
2022
            $templatecontext->columncollapse = $report->get_hide_show_link();
2023
            $templatecontext->dataid = $element['name'];
2024
        }
2025
 
2026
        // We do not want grade category total items to be hidden away as it is controlled by something else.
2027
        if (isset($element['object']->id) && !$element['object']->is_aggregate_item()) {
2028
            $templatecontext->columncollapse = $report->get_hide_show_link();
2029
        }
2030
        return $templatecontext;
2031
    }
2032
    return null;
2033
}