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
namespace gradereport_user\report;
18
 
19
use cache;
20
use context_course;
1441 ariadna 21
use core_grades\penalty_manager;
1 efrain 22
use course_modinfo;
23
use grade_grade;
24
use grade_helper;
25
use grade_item;
26
use grade_report;
27
use grade_tree;
28
use html_writer;
29
use moodle_url;
30
 
31
defined('MOODLE_INTERNAL') || die;
32
 
33
global $CFG;
34
require_once($CFG->dirroot.'/grade/report/lib.php');
35
require_once($CFG->dirroot.'/grade/lib.php');
36
 
37
/**
38
 * Class providing an API for the user report building and displaying.
39
 * @uses grade_report
40
 * @package gradereport_user
41
 */
42
class user extends grade_report {
43
 
44
    /**
45
     * A flexitable to hold the data.
46
     * @var object $table
47
     */
48
    public $table;
49
 
50
    /**
51
     * An array of table headers
52
     * @var array
53
     */
54
    public $tableheaders = [];
55
 
56
    /**
57
     * An array of table columns
58
     * @var array
59
     */
60
    public $tablecolumns = [];
61
 
62
    /**
63
     * An array containing rows of data for the table.
64
     * @var array
65
     */
66
    public $tabledata = [];
67
 
68
    /**
69
     * An array containing the grade items data for external usage (web services, ajax, etc...)
70
     * @var array
71
     */
72
    public $gradeitemsdata = [];
73
 
74
    /**
75
     * The grade tree structure
76
     * @var grade_tree
77
     */
78
    public $gtree;
79
 
80
    /**
81
     * Flat structure similar to grade tree
82
     * @var void
83
     */
84
    public $gseq;
85
 
86
    /**
87
     * show student ranks
88
     * @var void
89
     */
90
    public $showrank;
91
 
92
    /**
93
     * show grade percentages
94
     * @var void
95
     */
96
    public $showpercentage;
97
 
98
    /**
99
     * Show range
100
     * @var bool
101
     */
102
    public $showrange = true;
103
 
104
    /**
105
     * Show grades in the report, default true
106
     * @var bool
107
     */
108
    public $showgrade = true;
109
 
110
    /**
111
     * Decimal points to use for values in the report, default 2
112
     * @var int
113
     */
114
    public $decimals = 2;
115
 
116
    /**
117
     * The number of decimal places to round range to, default 0
118
     * @var int
119
     */
120
    public $rangedecimals = 0;
121
 
122
    /**
123
     * Show grade feedback in the report, default true
124
     * @var bool
125
     */
126
    public $showfeedback = true;
127
 
128
    /**
129
     * Show grade weighting in the report, default true.
130
     * @var bool
131
     */
132
    public $showweight = true;
133
 
134
    /**
135
     * Show letter grades in the report, default false
136
     * @var bool
137
     */
138
    public $showlettergrade = false;
139
 
140
    /**
141
     * Show the calculated contribution to the course total column.
142
     * @var bool
143
     */
144
    public $showcontributiontocoursetotal = true;
145
 
146
    /**
147
     * Show average grades in the report, default false.
148
     * @var false
149
     */
150
    public $showaverage = false;
151
 
152
    /**
153
     * @var int
154
     */
155
    public $maxdepth;
156
    /**
157
     * @var void
158
     */
159
    public $evenodd;
160
 
161
    /**
162
     * @var bool
163
     */
164
    public $canviewhidden;
165
 
166
    /**
167
     * @var string|null
168
     */
169
    public $switch;
170
 
171
    /**
172
     * Show hidden items even when user does not have required cap
173
     * @var void
174
     */
175
    public $showhiddenitems;
176
 
177
    /**
178
     * @var string
179
     */
180
    public $baseurl;
181
    /**
182
     * @var string
183
     */
184
    public $pbarurl;
185
 
186
    /**
187
     * The modinfo object to be used.
188
     *
189
     * @var course_modinfo
190
     */
191
    protected $modinfo = null;
192
 
193
    /**
194
     * View as user.
195
     *
196
     * When this is set to true, the visibility checks, and capability checks will be
197
     * applied to the user whose grades are being displayed. This is very useful when
198
     * a mentor/parent is viewing the report of their mentee because they need to have
199
     * access to the same information, but not more, not less.
200
     *
201
     * @var boolean
202
     */
203
    protected $viewasuser = false;
204
 
205
    /**
206
     * An array that collects the aggregationhints for every
207
     * grade_item. The hints contain grade, grademin, grademax
208
     * status, weight and parent.
209
     *
210
     * @var array
211
     */
212
    protected $aggregationhints = [];
213
 
214
    /**
215
     * Used for proper column indentation.
216
     * @var int
217
     */
218
    public $columncount = 0;
219
 
220
    /**
221
     * Constructor. Sets local copies of user preferences and initialises grade_tree.
222
     * @param int $courseid
223
     * @param null|object $gpr grade plugin return tracking object
224
     * @param object $context
225
     * @param int $userid The id of the user
226
     * @param bool $viewasuser Set this to true when the current user is a mentor/parent of the targetted user.
227
     */
1441 ariadna 228
    public function __construct(int $courseid, ?object $gpr, object $context, int $userid, ?bool $viewasuser = null) {
1 efrain 229
        global $DB, $CFG;
230
        parent::__construct($courseid, $gpr, $context);
231
 
232
        $this->showrank = grade_get_setting($this->courseid, 'report_user_showrank', $CFG->grade_report_user_showrank);
233
        $this->showpercentage = grade_get_setting(
234
            $this->courseid,
235
            'report_user_showpercentage',
236
            $CFG->grade_report_user_showpercentage
237
        );
238
        $this->showhiddenitems = grade_get_setting(
239
            $this->courseid,
240
            'report_user_showhiddenitems',
241
            $CFG->grade_report_user_showhiddenitems
242
        );
243
        $this->showtotalsifcontainhidden = [$this->courseid => grade_get_setting(
244
            $this->courseid,
245
            'report_user_showtotalsifcontainhidden',
246
            $CFG->grade_report_user_showtotalsifcontainhidden
247
        )];
248
 
249
        $this->showgrade = grade_get_setting(
250
            $this->courseid,
251
            'report_user_showgrade',
252
            !empty($CFG->grade_report_user_showgrade)
253
        );
254
        $this->showrange = grade_get_setting(
255
            $this->courseid,
256
            'report_user_showrange',
257
            !empty($CFG->grade_report_user_showrange)
258
        );
259
        $this->showfeedback = grade_get_setting(
260
            $this->courseid,
261
            'report_user_showfeedback',
262
            !empty($CFG->grade_report_user_showfeedback)
263
        );
264
 
265
        $this->showweight = grade_get_setting($this->courseid, 'report_user_showweight',
266
            !empty($CFG->grade_report_user_showweight));
267
 
268
        $this->showcontributiontocoursetotal = grade_get_setting($this->courseid, 'report_user_showcontributiontocoursetotal',
269
            !empty($CFG->grade_report_user_showcontributiontocoursetotal));
270
 
271
        $this->showlettergrade = grade_get_setting(
272
            $this->courseid,
273
            'report_user_showlettergrade',
274
            !empty($CFG->grade_report_user_showlettergrade)
275
        );
276
        $this->showaverage = grade_get_setting(
277
            $this->courseid,
278
            'report_user_showaverage',
279
            !empty($CFG->grade_report_user_showaverage)
280
        );
281
 
282
        $this->viewasuser = $viewasuser;
283
 
284
        // The default grade decimals is 2.
285
        $defaultdecimals = 2;
286
        if (property_exists($CFG, 'grade_decimalpoints')) {
287
            $defaultdecimals = $CFG->grade_decimalpoints;
288
        }
289
        $this->decimals = grade_get_setting($this->courseid, 'decimalpoints', $defaultdecimals);
290
 
291
        // The default range decimals is 0.
292
        $defaultrangedecimals = 0;
293
        if (property_exists($CFG, 'grade_report_user_rangedecimals')) {
294
            $defaultrangedecimals = $CFG->grade_report_user_rangedecimals;
295
        }
296
        $this->rangedecimals = grade_get_setting($this->courseid, 'report_user_rangedecimals', $defaultrangedecimals);
297
 
298
        $this->switch = grade_get_setting($this->courseid, 'aggregationposition', $CFG->grade_aggregationposition);
299
 
300
        // Grab the grade_tree for this course.
301
        $this->gtree = new grade_tree($this->courseid, false, $this->switch, null, !$CFG->enableoutcomes);
302
 
303
        // Get the user (for full name).
304
        $this->user = $DB->get_record('user', ['id' => $userid]);
305
 
306
        // What user are we viewing this as?
307
        $coursecontext = context_course::instance($this->courseid);
308
        if ($viewasuser) {
309
            $this->modinfo = new course_modinfo($this->course, $this->user->id);
310
            $this->canviewhidden = has_capability('moodle/grade:viewhidden', $coursecontext, $this->user->id);
311
        } else {
312
            $this->modinfo = $this->gtree->modinfo;
313
            $this->canviewhidden = has_capability('moodle/grade:viewhidden', $coursecontext);
314
        }
315
 
316
        // Determine the number of rows and indentation.
317
        $this->maxdepth = 1;
318
        $this->inject_rowspans($this->gtree->top_element);
319
        $this->maxdepth++; // Need to account for the lead column that spans all children.
320
        for ($i = 1; $i <= $this->maxdepth; $i++) {
321
            $this->evenodd[$i] = 0;
322
        }
323
 
324
        $this->tabledata = [];
325
 
326
        // The base url for sorting by first/last name.
327
        $this->baseurl = new \moodle_url('/grade/report', ['id' => $courseid, 'userid' => $userid]);
328
        $this->pbarurl = $this->baseurl;
329
 
330
        // There no groups on this report - rank is from all course users.
331
        $this->setup_table();
332
 
333
        // Optionally calculate grade item averages.
334
        if ($this->showaverage) {
335
            $cache = \cache::make_from_params(\cache_store::MODE_REQUEST, 'gradereport_user', 'averages');
336
            $avg = $cache->get(get_class($this));
337
            if (!$avg) {
338
                $showonlyactiveenrol = $this->show_only_active();
339
                $ungradedcounts = $this->ungraded_counts(false, false, $showonlyactiveenrol);
340
                $this->columncount = 0;
341
                $avgrow = $this->format_averages($ungradedcounts);
342
                // Save to cache.
343
                $cache->set(get_class($this), $avgrow->cells);
344
            }
345
        }
346
 
347
    }
348
 
349
    /**
350
     * Returns a row of grade items averages
351
     *
352
     * @param grade_item $gradeitem Grade item.
353
     * @param array|null $aggr Average value and meancount information.
354
     * @param bool|null $shownumberofgrades Whether to show number of grades.
355
     * @return \html_table_cell Formatted average cell.
356
     */
357
    protected function format_average_cell(grade_item $gradeitem, ?array $aggr = null, ?bool $shownumberofgrades = null): \html_table_cell {
358
 
359
        if ($gradeitem->needsupdate) {
360
            $avg = '<td class="cell c' . $this->columncount++.'">' .
361
                '<span class="gradingerror">' . get_string('error').'</span></td>';
362
        } else {
363
            if (empty($aggr['average'])) {
364
                $avg = '-';
365
            } else {
366
                $numberofgrades = '';
367
                if ($shownumberofgrades) {
368
                    $numberofgrades = " (" . $aggr['meancount'] . ")";
369
                }
370
                $avg = $aggr['average'] . $numberofgrades;
371
            }
372
        }
373
        return new \html_table_cell($avg);
374
    }
375
 
376
    /**
377
     * Recurse through a tree of elements setting the rowspan property on each element
378
     *
379
     * @param array $element Either the top element or, during recursion, the current element
380
     * @return int The number of elements processed
381
     */
382
    public function inject_rowspans(array &$element): int {
383
 
384
        if ($element['depth'] > $this->maxdepth) {
385
            $this->maxdepth = $element['depth'];
386
        }
387
        if (empty($element['children'])) {
388
            return 1;
389
        }
390
        $count = 1;
391
 
392
        foreach ($element['children'] as $key => $child) {
393
            // If category is hidden then do not include it in the rowspan.
394
            if ($child['type'] == 'category' && $child['object']->is_hidden() && !$this->canviewhidden
395
                && ($this->showhiddenitems == GRADE_REPORT_USER_HIDE_HIDDEN
396
                    || ($this->showhiddenitems == GRADE_REPORT_USER_HIDE_UNTIL && !$child['object']->is_hiddenuntil()))) {
397
                // Just calculate the rowspans for children of this category, don't add them to the count.
398
                $this->inject_rowspans($element['children'][$key]);
399
            } else {
400
                $count += $this->inject_rowspans($element['children'][$key]);
401
                // Take into consideration the addition of a new row (where the rowspan is defined) right after a category row.
402
                if ($child['type'] == 'category') {
403
                    $count += 1;
404
                }
405
 
406
            }
407
        }
408
 
409
        $element['rowspan'] = $count;
410
        return $count;
411
    }
412
 
413
 
414
    /**
415
     * Prepares the headers and attributes of the flexitable.
416
     */
417
    public function setup_table() {
418
        /*
419
         * Table has 1-8 columns
420
         *| All columns except for itemname/description are optional
421
         */
422
 
423
        // Setting up table headers.
424
 
425
        $this->tablecolumns = ['itemname'];
426
        $this->tableheaders = [get_string('gradeitem', 'grades')];
427
 
428
        if ($this->showweight) {
429
            $this->tablecolumns[] = 'weight';
430
            $this->tableheaders[] = get_string('weightuc', 'grades');
431
        }
432
 
433
        if ($this->showgrade) {
434
            $this->tablecolumns[] = 'grade';
435
            $this->tableheaders[] = get_string('gradenoun');
436
        }
437
 
438
        if ($this->showrange) {
439
            $this->tablecolumns[] = 'range';
440
            $this->tableheaders[] = get_string('range', 'grades');
441
        }
442
 
443
        if ($this->showpercentage) {
444
            $this->tablecolumns[] = 'percentage';
445
            $this->tableheaders[] = get_string('percentage', 'grades');
446
        }
447
 
448
        if ($this->showlettergrade) {
449
            $this->tablecolumns[] = 'lettergrade';
450
            $this->tableheaders[] = get_string('lettergrade', 'grades');
451
        }
452
 
453
        if ($this->showrank) {
454
            $this->tablecolumns[] = 'rank';
455
            $this->tableheaders[] = get_string('rank', 'grades');
456
        }
457
 
458
        if ($this->showaverage) {
459
            $this->tablecolumns[] = 'average';
460
            $this->tableheaders[] = get_string('average', 'grades');
461
        }
462
 
463
        if ($this->showfeedback) {
464
            $this->tablecolumns[] = 'feedback';
465
            $this->tableheaders[] = get_string('feedback', 'grades');
466
        }
467
 
468
        if ($this->showcontributiontocoursetotal) {
469
            $this->tablecolumns[] = 'contributiontocoursetotal';
470
            $this->tableheaders[] = get_string('contributiontocoursetotal', 'grades');
471
        }
472
    }
473
 
474
    /**
475
     * Provide an entry point to build the table.
476
     *
477
     * @return bool
478
     */
479
    public function fill_table(): bool {
480
        $this->fill_table_recursive($this->gtree->top_element);
481
        return true;
482
    }
483
 
484
    /**
485
     * Fill the table with data.
486
     *
487
     * @param array $element - The table data for the current row.
488
     */
489
    private function fill_table_recursive(array &$element) {
490
        global $DB, $CFG, $OUTPUT;
491
 
492
        $type = $element['type'];
493
        $depth = $element['depth'];
494
        $gradeobject = $element['object'];
495
        $eid = $gradeobject->id;
496
        $element['userid'] = $userid = $this->user->id;
497
        $fullname = grade_helper::get_element_header($element, true, false, true, false, true);
498
        $data = [];
499
        $gradeitemdata = [];
500
        $hidden = '';
501
        $excluded = '';
502
        $itemlevel = ($type == 'categoryitem' || $type == 'category' || $type == 'courseitem') ? $depth : ($depth + 1);
503
        $class = 'level' . $itemlevel;
504
        $classfeedback = '';
505
        $rowspandata = [];
506
 
507
        // If this is a hidden grade category, hide it completely from the user.
508
        if ($type == 'category' && $gradeobject->is_hidden() && !$this->canviewhidden && (
509
                $this->showhiddenitems == GRADE_REPORT_USER_HIDE_HIDDEN ||
510
                ($this->showhiddenitems == GRADE_REPORT_USER_HIDE_UNTIL && !$gradeobject->is_hiddenuntil()))) {
511
            return false;
512
        }
513
 
514
        // Process those items that have scores associated.
515
        if ($type == 'item' || $type == 'categoryitem' || $type == 'courseitem') {
516
            $headerrow = "row_{$eid}_{$this->user->id}";
517
            $headercat = "cat_{$gradeobject->categoryid}_{$this->user->id}";
518
 
519
            if (! $gradegrade = grade_grade::fetch(['itemid' => $gradeobject->id, 'userid' => $this->user->id])) {
520
                $gradegrade = new grade_grade();
521
                $gradegrade->userid = $this->user->id;
522
                $gradegrade->itemid = $gradeobject->id;
523
            }
524
 
525
            $gradegrade->load_grade_item();
526
 
527
            // Hidden Items.
1441 ariadna 528
            if ($gradegrade->grade_item->is_hidden() && $this->canviewhidden) {
1 efrain 529
                $hidden = ' dimmed_text';
530
            }
531
 
532
            $hide = false;
533
            // If this is a hidden grade item, hide it completely from the user.
534
            if ($gradegrade->is_hidden() && !$this->canviewhidden && (
535
                    $this->showhiddenitems == GRADE_REPORT_USER_HIDE_HIDDEN ||
536
                    ($this->showhiddenitems == GRADE_REPORT_USER_HIDE_UNTIL && !$gradegrade->is_hiddenuntil()))) {
537
                $hide = true;
538
            } else if (!empty($gradeobject->itemmodule) && !empty($gradeobject->iteminstance)) {
539
                // The grade object can be marked visible but still be hidden if
540
                // the student cannot see the activity due to conditional access
541
                // and it's set to be hidden entirely.
542
                $instances = $this->modinfo->get_instances_of($gradeobject->itemmodule);
543
                if (!empty($instances[$gradeobject->iteminstance])) {
544
                    $cm = $instances[$gradeobject->iteminstance];
545
                    $gradeitemdata['cmid'] = $cm->id;
546
                    if (!$cm->uservisible) {
547
                        // If there is 'availableinfo' text then it is only greyed
548
                        // out and not entirely hidden.
549
                        if (!$cm->availableinfo) {
550
                            $hide = true;
551
                        }
552
                    }
553
                }
554
            }
555
 
556
            // Actual Grade - We need to calculate this whether the row is hidden or not.
557
            $gradeval = $gradegrade->finalgrade;
558
            $hint = $gradegrade->get_aggregation_hint();
559
            if (!$this->canviewhidden) {
560
                // Virtual Grade (may be calculated excluding hidden items etc).
561
                $adjustedgrade = $this->blank_hidden_total_and_adjust_bounds($this->courseid,
562
                    $gradegrade->grade_item,
563
                    $gradeval);
564
 
565
                $gradeval = $adjustedgrade['grade'];
566
 
567
                // We temporarily adjust the view of this grade item - because the min and
568
                // max are affected by the hidden values in the aggregation.
569
                $gradegrade->grade_item->grademax = $adjustedgrade['grademax'];
570
                $gradegrade->grade_item->grademin = $adjustedgrade['grademin'];
571
                $hint['status'] = $adjustedgrade['aggregationstatus'];
572
                $hint['weight'] = $adjustedgrade['aggregationweight'];
573
            } else {
574
                // The max and min for an aggregation may be different to the grade_item.
575
                if (!is_null($gradeval)) {
576
                    $gradegrade->grade_item->grademax = $gradegrade->get_grade_max();
577
                    $gradegrade->grade_item->grademin = $gradegrade->get_grade_min();
578
                }
579
            }
580
 
581
            if (!$hide) {
582
                $canviewall = has_capability('moodle/grade:viewall', $this->context);
583
                // Other class information.
584
                $class .= $hidden . $excluded;
585
                // Alter style based on whether aggregation is first or last.
586
                if ($this->switch) {
587
                    $class .= ($type == 'categoryitem' || $type == 'courseitem') ? " d$depth baggt b2b" : " item b1b";
588
                } else {
589
                    $class .= ($type == 'categoryitem' || $type == 'courseitem') ? " d$depth baggb" : " item b1b";
590
                }
591
 
1441 ariadna 592
                $itemicon = \html_writer::div(grade_helper::get_element_icon($element), 'me-1');
1 efrain 593
                $elementtype = grade_helper::get_element_type_string($element);
1441 ariadna 594
                $itemtype = \html_writer::span($elementtype, 'd-block text-uppercase small ' . $hidden,
1 efrain 595
                    ['title' => $elementtype]);
596
 
597
                if ($type == 'categoryitem' || $type == 'courseitem') {
598
                    $headercat = "cat_{$gradeobject->iteminstance}_{$this->user->id}";
599
                }
600
 
601
                // Generate the content for a cell that represents a grade item.
602
                $itemtitle = \html_writer::div($fullname, 'rowtitle');
603
                $content = \html_writer::div($itemtype . $itemtitle);
604
 
605
                // Name.
606
                $data['itemname']['content'] = \html_writer::div($itemicon . $content, "{$type} d-flex align-items-center");
607
                $data['itemname']['class'] = $class;
608
                $data['itemname']['colspan'] = ($this->maxdepth - $depth);
609
                $data['itemname']['id'] = $headerrow;
610
 
611
                // Basic grade item information.
612
                $gradeitemdata['id'] = $gradeobject->id;
613
                $gradeitemdata['itemname'] = $gradeobject->itemname;
614
                $gradeitemdata['itemtype'] = $gradeobject->itemtype;
615
                $gradeitemdata['itemmodule'] = $gradeobject->itemmodule;
616
                $gradeitemdata['iteminstance'] = $gradeobject->iteminstance;
617
                $gradeitemdata['itemnumber'] = $gradeobject->itemnumber;
618
                $gradeitemdata['idnumber'] = $gradeobject->idnumber;
619
                $gradeitemdata['categoryid'] = $gradeobject->categoryid;
620
                $gradeitemdata['outcomeid'] = $gradeobject->outcomeid;
621
                $gradeitemdata['scaleid'] = $gradeobject->outcomeid;
622
                $gradeitemdata['locked'] = $canviewall ? $gradegrade->grade_item->is_locked() : null;
623
 
624
                if ($this->showfeedback) {
625
                    // Copy $class before appending itemcenter as feedback should not be centered.
626
                    $classfeedback = $class;
627
                }
628
                $class .= " itemcenter ";
629
                if ($this->showweight) {
630
                    $data['weight']['class'] = $class;
631
                    $data['weight']['content'] = '-';
632
                    $data['weight']['headers'] = "$headercat $headerrow weight$userid";
633
                    // Has a weight assigned, might be extra credit.
634
 
635
                    // This obliterates the weight because it provides a more informative description.
636
                    if (is_numeric($hint['weight'])) {
637
                        $data['weight']['content'] = format_float($hint['weight'] * 100.0, 2) . ' %';
638
                        $gradeitemdata['weightraw'] = $hint['weight'];
639
                        $gradeitemdata['weightformatted'] = $data['weight']['content'];
640
                    }
641
                    if ($hint['status'] != 'used' && $hint['status'] != 'unknown') {
642
                        $data['weight']['content'] .= '<br>' . get_string('aggregationhint' . $hint['status'], 'grades');
643
                        $gradeitemdata['status'] = $hint['status'];
644
                    }
645
                }
646
 
647
                if ($this->showgrade) {
648
                    $gradestatus = '';
649
                    // We only show status icons for a teacher if he views report as himself.
650
                    if (isset($this->viewasuser) && !$this->viewasuser) {
651
                        $context = [
652
                            'hidden' => $gradegrade->is_hidden(),
653
                            'locked' => $gradegrade->is_locked(),
654
                            'overridden' => $gradegrade->is_overridden(),
655
                            'excluded' => $gradegrade->is_excluded()
656
                        ];
657
 
658
                        if (in_array(true, $context)) {
659
                            $context['classes'] = 'gradestatus';
660
                            $gradestatus = $OUTPUT->render_from_template('core_grades/status_icons', $context);
661
                        }
662
                    }
663
 
664
                    $gradeitemdata['graderaw'] = null;
665
                    $gradeitemdata['gradehiddenbydate'] = false;
666
                    $gradeitemdata['gradeneedsupdate'] = $gradegrade->grade_item->needsupdate;
667
                    $gradeitemdata['gradeishidden'] = $gradegrade->is_hidden();
668
                    $gradeitemdata['gradedatesubmitted'] = $gradegrade->get_datesubmitted();
669
                    $gradeitemdata['gradedategraded'] = $gradegrade->get_dategraded();
670
                    $gradeitemdata['gradeislocked'] = $canviewall ? $gradegrade->is_locked() : null;
671
                    $gradeitemdata['gradeisoverridden'] = $canviewall ? $gradegrade->is_overridden() : null;
672
 
673
                    if ($gradegrade->grade_item->needsupdate) {
674
                        $data['grade']['class'] = $class.' gradingerror';
675
                        $data['grade']['content'] = get_string('error');
676
                    } else if (
677
                        !empty($CFG->grade_hiddenasdate)
678
                        && $gradegrade->get_datesubmitted()
679
                        && !$this->canviewhidden
680
                        && $gradegrade->is_hidden()
681
                        && !$gradegrade->grade_item->is_category_item()
682
                        && !$gradegrade->grade_item->is_course_item()
683
                    ) {
684
                        // The problem here is that we do not have the time when grade value was modified
685
                        // 'timemodified' is general modification date for grade_grades records.
686
                        $class .= ' datesubmitted';
687
                        $data['grade']['class'] = $class;
688
                        $data['grade']['content'] = get_string(
689
                            'submittedon',
690
                            'grades',
691
                            userdate(
692
                                $gradegrade->get_datesubmitted(),
693
                                get_string('strftimedatetimeshort')
694
                            ) . $gradestatus
695
                        );
696
                        $gradeitemdata['gradehiddenbydate'] = true;
697
                    } else if ($gradegrade->is_hidden()) {
1441 ariadna 698
                        $data['grade']['class'] = $class;
1 efrain 699
                        $data['grade']['content'] = '-';
700
 
701
                        if ($this->canviewhidden) {
1441 ariadna 702
                            $data['grade']['class'] .= ' dimmed_text';
1 efrain 703
                            $gradeitemdata['graderaw'] = $gradeval;
704
                            $data['grade']['content'] = grade_format_gradevalue($gradeval,
705
                                $gradegrade->grade_item,
1441 ariadna 706
                                true) . penalty_manager::show_penalty_indicator($gradegrade) . $gradestatus;
1 efrain 707
                        }
708
                    } else {
709
                        $gradestatusclass = '';
710
                        $gradepassicon = '';
711
                        $ispassinggrade = $gradegrade->is_passed($gradegrade->grade_item);
712
                        if (!is_null($gradeval) && !is_null($ispassinggrade)) {
713
                            $gradestatusclass = $ispassinggrade ? 'gradepass' : 'gradefail';
714
                            if ($ispassinggrade) {
715
                                $gradepassicon = $OUTPUT->pix_icon(
716
                                    'i/valid',
717
                                    get_string('pass', 'grades'),
718
                                    null,
719
                                    ['class' => 'inline']
720
                                );
721
                            } else {
722
                                $gradepassicon = $OUTPUT->pix_icon(
723
                                    'i/invalid',
724
                                    get_string('fail', 'grades'),
725
                                    null,
726
                                    ['class' => 'inline']
727
                                );
728
                            }
729
                        }
730
 
731
                        $data['grade']['class'] = "{$class} {$gradestatusclass}";
732
                        $data['grade']['content'] = $gradepassicon . grade_format_gradevalue($gradeval,
1441 ariadna 733
                            $gradegrade->grade_item, true) . penalty_manager::show_penalty_indicator($gradegrade) . $gradestatus;
1 efrain 734
                        $gradeitemdata['graderaw'] = $gradeval;
735
                    }
736
                    $data['grade']['headers'] = "$headercat $headerrow grade$userid";
737
                    $gradeitemdata['gradeformatted'] = $data['grade']['content'];
738
                    // If the current grade item need to show a grade action menu, generate the appropriate output.
739
                    if ($gradeactionmenu = $this->gtree->get_grade_action_menu($gradegrade)) {
740
                        $gradecontainer = html_writer::div($data['grade']['content']);
1441 ariadna 741
                        $grademenucontainer = html_writer::div($gradeactionmenu, 'ps-1 d-flex align-items-center');
1 efrain 742
                        $data['grade']['content'] = html_writer::div($gradecontainer . $grademenucontainer,
743
                            'd-flex align-items-center');
744
                    }
745
                }
746
 
747
                // Range.
748
                if ($this->showrange) {
749
                    $data['range']['class'] = $class;
750
                    $data['range']['content'] = $gradegrade->grade_item->get_formatted_range(
751
                        GRADE_DISPLAY_TYPE_REAL,
752
                        $this->rangedecimals
753
                    );
754
                    $data['range']['headers'] = "$headercat $headerrow range$userid";
755
 
756
                    $gradeitemdata['rangeformatted'] = $data['range']['content'];
757
                    $gradeitemdata['grademin'] = $gradegrade->grade_item->grademin;
758
                    $gradeitemdata['grademax'] = $gradegrade->grade_item->grademax;
759
                }
760
 
761
                // Percentage.
762
                if ($this->showpercentage) {
763
                    if ($gradegrade->grade_item->needsupdate) {
764
                        $data['percentage']['class'] = $class.' gradingerror';
765
                        $data['percentage']['content'] = get_string('error');
766
                    } else if ($gradegrade->is_hidden()) {
1441 ariadna 767
                        $data['percentage']['class'] = $class;
1 efrain 768
                        $data['percentage']['content'] = '-';
769
                        if ($this->canviewhidden) {
1441 ariadna 770
                            $data['percentage']['class'] .= ' dimmed_text';
1 efrain 771
                            $data['percentage']['content'] = grade_format_gradevalue(
772
                                $gradeval,
773
                                $gradegrade->grade_item,
774
                                true,
775
                                GRADE_DISPLAY_TYPE_PERCENTAGE
776
                            );
777
                        }
778
                    } else {
779
                        $data['percentage']['class'] = $class;
780
                        $data['percentage']['content'] = grade_format_gradevalue(
781
                            $gradeval,
782
                            $gradegrade->grade_item,
783
                            true,
784
                            GRADE_DISPLAY_TYPE_PERCENTAGE
785
                        );
786
                    }
787
                    $data['percentage']['headers'] = "$headercat $headerrow percentage$userid";
788
                    $gradeitemdata['percentageformatted'] = $data['percentage']['content'];
789
                }
790
 
791
                // Lettergrade.
792
                if ($this->showlettergrade) {
793
                    if ($gradegrade->grade_item->needsupdate) {
794
                        $data['lettergrade']['class'] = $class.' gradingerror';
795
                        $data['lettergrade']['content'] = get_string('error');
796
                    } else if ($gradegrade->is_hidden()) {
1441 ariadna 797
                        $data['lettergrade']['class'] = $class;
1 efrain 798
                        if (!$this->canviewhidden) {
1441 ariadna 799
                            $data['lettergrade']['class'] .= ' dimmed_text';
1 efrain 800
                            $data['lettergrade']['content'] = '-';
801
                        } else {
802
                            $data['lettergrade']['content'] = grade_format_gradevalue(
803
                                $gradeval,
804
                                $gradegrade->grade_item,
805
                                true,
806
                                GRADE_DISPLAY_TYPE_LETTER
807
                            );
808
                        }
809
                    } else {
810
                        $data['lettergrade']['class'] = $class;
811
                        $data['lettergrade']['content'] = grade_format_gradevalue(
812
                            $gradeval,
813
                            $gradegrade->grade_item,
814
                            true,
815
                            GRADE_DISPLAY_TYPE_LETTER
816
                        );
817
                    }
818
                    $data['lettergrade']['headers'] = "$headercat $headerrow lettergrade$userid";
819
                    $gradeitemdata['lettergradeformatted'] = $data['lettergrade']['content'];
820
                }
821
 
822
                // Rank.
823
                if ($this->showrank) {
824
                    $gradeitemdata['rank'] = 0;
825
                    if ($gradegrade->grade_item->needsupdate) {
826
                        $data['rank']['class'] = $class.' gradingerror';
827
                        $data['rank']['content'] = get_string('error');
828
                    } else if ($gradegrade->is_hidden()) {
1441 ariadna 829
                        $data['rank']['class'] = $class;
1 efrain 830
                        $data['rank']['content'] = '-';
1441 ariadna 831
                        if ($this->canviewhidden) {
832
                            $data['rank']['class'] .= ' dimmed_text';
833
                        }
1 efrain 834
                    } else if (is_null($gradeval)) {
835
                        // No grade, o rank.
836
                        $data['rank']['class'] = $class;
837
                        $data['rank']['content'] = '-';
838
 
839
                    } else {
840
                        // Find the number of users with a higher grade.
841
                        $sql = "SELECT COUNT(DISTINCT(userid))
842
                                  FROM {grade_grades}
843
                                 WHERE finalgrade > ?
844
                                       AND itemid = ?
845
                                       AND hidden = 0";
846
                        $rank = $DB->count_records_sql($sql, [$gradegrade->finalgrade, $gradegrade->grade_item->id]) + 1;
847
 
848
                        $data['rank']['class'] = $class;
849
                        $numusers = $this->get_numusers(false);
850
                        $data['rank']['content'] = "$rank/$numusers"; // Total course users.
851
 
852
                        $gradeitemdata['rank'] = $rank;
853
                        $gradeitemdata['numusers'] = $numusers;
854
                    }
855
                    $data['rank']['headers'] = "$headercat $headerrow rank$userid";
856
                }
857
 
858
                // Average.
859
                if ($this->showaverage) {
860
                    $data['average']['class'] = $class;
861
                    $cache = \cache::make_from_params(\cache_store::MODE_REQUEST, 'gradereport_user', 'averages');
862
                    $avg = $cache->get(get_class($this));
863
 
864
                    $data['average']['content'] = $avg[$eid]->text;;
865
                    $gradeitemdata['averageformatted'] = $avg[$eid]->text;;
866
                    $data['average']['headers'] = "$headercat $headerrow average$userid";
867
                }
868
 
869
                // Feedback.
870
                if ($this->showfeedback) {
871
                    $gradeitemdata['feedback'] = '';
872
                    $gradeitemdata['feedbackformat'] = $gradegrade->feedbackformat;
873
 
874
                    if ($gradegrade->feedback) {
875
                        $gradegrade->feedback = file_rewrite_pluginfile_urls(
876
                            $gradegrade->feedback,
877
                            'pluginfile.php',
878
                            $gradegrade->get_context()->id,
879
                            GRADE_FILE_COMPONENT,
880
                            GRADE_FEEDBACK_FILEAREA,
881
                            $gradegrade->id
882
                        );
883
                    }
884
 
885
                    $data['feedback']['class'] = $classfeedback.' feedbacktext';
886
                    if (empty($gradegrade->feedback) || (!$this->canviewhidden && $gradegrade->is_hidden())) {
887
                        $data['feedback']['content'] = '&nbsp;';
888
                    } else {
889
                        $data['feedback']['content'] = format_text($gradegrade->feedback, $gradegrade->feedbackformat,
890
                            ['context' => $gradegrade->get_context()]);
891
                        $gradeitemdata['feedback'] = $gradegrade->feedback;
892
                    }
893
                    $data['feedback']['headers'] = "$headercat $headerrow feedback$userid";
894
                }
895
                // Contribution to the course total column.
896
                if ($this->showcontributiontocoursetotal) {
897
                    $data['contributiontocoursetotal']['class'] = $class;
898
                    $data['contributiontocoursetotal']['content'] = '-';
899
                    $data['contributiontocoursetotal']['headers'] = "$headercat $headerrow contributiontocoursetotal$userid";
900
 
901
                }
902
                $this->gradeitemsdata[] = $gradeitemdata;
903
            }
904
 
905
            $parent = $gradeobject->load_parent_category();
906
            if ($gradeobject->is_category_item()) {
907
                $parent = $parent->load_parent_category();
908
            }
909
 
910
            // We collect the aggregation hints whether they are hidden or not.
911
            if ($this->showcontributiontocoursetotal) {
912
                $hint['grademax'] = $gradegrade->grade_item->grademax;
913
                $hint['grademin'] = $gradegrade->grade_item->grademin;
914
                $hint['grade'] = $gradeval;
915
                $hint['parent'] = $parent->load_grade_item()->id;
916
                $this->aggregationhints[$gradegrade->itemid] = $hint;
917
            }
918
            // Get the IDs of all parent categories of this grading item.
919
            $data['parentcategories'] = array_filter(explode('/', $gradeobject->parent_category->path));
920
        }
921
 
922
        // Category.
923
        if ($type == 'category') {
924
            // Determine directionality so that icons can be modified to suit language.
925
            $arrow = right_to_left() ? 'left' : 'right';
926
            // Alter style based on whether aggregation is first or last.
927
            if ($this->switch) {
928
                $data['itemname']['class'] = $class . ' ' . "d$depth b1b b1t category";
929
            } else {
930
                $data['itemname']['class'] = $class . ' ' . "d$depth b2t category";
931
            }
932
            $data['itemname']['colspan'] = ($this->maxdepth - $depth + count($this->tablecolumns));
933
            $data['itemname']['content'] = $OUTPUT->render_from_template('gradereport_user/user_report_category_content',
934
                ['categoryid' => $gradeobject->id, 'categoryname' => $fullname, 'arrow' => $arrow]);
935
            $data['itemname']['id'] = "cat_{$gradeobject->id}_{$this->user->id}";
936
            // Get the IDs of all parent categories of this grade category.
937
            $data['parentcategories'] = array_diff(array_filter(explode('/', $gradeobject->path)), [$gradeobject->id]);
938
 
939
            $rowspandata['leader']['class'] = $class . " d$depth b1t b2b b1l";
940
            $rowspandata['leader']['rowspan'] = $element['rowspan'];
941
            $rowspandata['parentcategories'] = array_filter(explode('/', $gradeobject->path));
942
            $rowspandata['spacer'] = true;
943
        }
944
 
945
        // Add this row to the overall system.
946
        foreach ($data as $key => $celldata) {
947
            if (isset($celldata['class'])) {
948
                $data[$key]['class'] .= ' column-' . $key;
949
            }
950
        }
951
 
952
        $this->tabledata[] = $data;
953
 
954
        if (!empty($rowspandata)) {
955
            $this->tabledata[] = $rowspandata;
956
        }
957
 
958
        // Recursively iterate through all child elements.
959
        if (isset($element['children'])) {
960
            foreach ($element['children'] as $key => $child) {
961
                $this->fill_table_recursive($element['children'][$key]);
962
            }
963
        }
964
 
965
        // Check we are showing this column, and we are looking at the root of the table.
966
        // This should be the very last thing this fill_table_recursive function does.
967
        if ($this->showcontributiontocoursetotal && ($type == 'category' && $depth == 1)) {
968
            // We should have collected all the hints by now - walk the tree again and build the contributions column.
969
            $this->fill_contributions_column($element);
970
        }
971
    }
972
 
973
    /**
974
     * This function is called after the table has been built and the aggregationhints
975
     * have been collected. We need this info to walk up the list of parents of each
976
     * grade_item.
977
     *
978
     * @param array $element - An array containing the table data for the current row.
979
     */
980
    public function fill_contributions_column(array $element) {
981
 
982
        // Recursively iterate through all child elements.
983
        if (isset($element['children'])) {
984
            foreach ($element['children'] as $key => $child) {
985
                $this->fill_contributions_column($element['children'][$key]);
986
            }
987
        } else if ($element['type'] == 'item') {
988
            // This is a grade item (We don't do this for categories or we would double count).
989
            $gradeobject = $element['object'];
990
            $itemid = $gradeobject->id;
991
 
992
            // Ignore anything with no hint - e.g. a hidden row.
993
            if (isset($this->aggregationhints[$itemid])) {
994
 
995
                // Normalise the gradeval.
996
                $gradecat = $gradeobject->load_parent_category();
997
                if ($gradecat->aggregation == GRADE_AGGREGATE_SUM) {
998
                    // Natural aggregation/Sum of grades does not consider the mingrade, cannot traditionnally normalise it.
999
                    $graderange = $this->aggregationhints[$itemid]['grademax'];
1000
 
1001
                    if ($graderange != 0) {
1002
                        $gradeval = $this->aggregationhints[$itemid]['grade'] / $graderange;
1003
                    } else {
1004
                        $gradeval = 0;
1005
                    }
1006
                } else {
1007
                    $gradeval = grade_grade::standardise_score(
1008
                        $this->aggregationhints[$itemid]['grade'],
1009
                        $this->aggregationhints[$itemid]['grademin'],
1010
                        $this->aggregationhints[$itemid]['grademax'],
1011
                        0,
1012
                        1
1013
                    );
1014
                }
1015
 
1016
                // Multiply the normalised value by the weight
1017
                // of all the categories higher in the tree.
1018
                $parent = null;
1019
                do {
1020
                    if (!is_null($this->aggregationhints[$itemid]['weight'])) {
1021
                        $gradeval *= $this->aggregationhints[$itemid]['weight'];
1022
                    } else if (empty($parent)) {
1023
                        // If we are in the first loop, and the weight is null, then we cannot calculate the contribution.
1024
                        $gradeval = null;
1025
                        break;
1026
                    }
1027
 
1028
                    // The second part of this if is to prevent infinite loops
1029
                    // in case of crazy data.
1030
                    if (isset($this->aggregationhints[$itemid]['parent']) &&
1031
                        $this->aggregationhints[$itemid]['parent'] != $itemid) {
1032
                        $parent = $this->aggregationhints[$itemid]['parent'];
1033
                        $itemid = $parent;
1034
                    } else {
1035
                        // We are at the top of the tree.
1036
                        $parent = false;
1037
                    }
1038
                } while ($parent);
1039
 
1040
                // Finally multiply by the course grademax.
1041
                if (!is_null($gradeval)) {
1042
                    // Convert to percent.
1043
                    $gradeval *= 100;
1044
                }
1045
 
1046
                // Now we need to loop through the "built" table data and update the
1047
                // contributions column for the current row.
1048
                $headerrow = "row_{$gradeobject->id}_{$this->user->id}";
1049
                foreach ($this->tabledata as $key => $row) {
1050
                    if (isset($row['itemname']) && ($row['itemname']['id'] == $headerrow)) {
1051
                        // Found it - update the column.
1052
                        $content = '-';
1053
                        if (!is_null($gradeval)) {
1054
                            $decimals = $gradeobject->get_decimals();
1055
                            $content = format_float($gradeval, $decimals, true) . ' %';
1056
                        }
1057
                        $this->tabledata[$key]['contributiontocoursetotal']['content'] = $content;
1058
                        break;
1059
                    }
1060
                }
1061
            }
1062
        }
1063
    }
1064
 
1065
    /**
1066
     * Prints or returns the HTML from the flexitable.
1067
     *
1068
     * @param bool $return Whether or not to return the data instead of printing it directly.
1069
     * @return string|void
1070
     */
1071
    public function print_table(bool $return = false) {
1072
        global $PAGE;
1073
 
1074
        $table = new \html_table();
1075
        $table->attributes = [
1076
            'summary' => s(get_string('tablesummary', 'gradereport_user')),
1077
            'class' => 'generaltable boxaligncenter user-grade',
1078
        ];
1079
 
1080
        // Set the table headings.
1081
        $userid = $this->user->id;
1082
        foreach ($this->tableheaders as $index => $heading) {
1083
            $headingcell = new \html_table_cell($heading);
1084
            $headingcell->attributes['id'] = $this->tablecolumns[$index] . $userid;
1085
            $headingcell->attributes['class'] = "header column-{$this->tablecolumns[$index]}";
1086
            if ($index == 0) {
1087
                $headingcell->colspan = $this->maxdepth;
1088
            }
1089
            $table->head[] = $headingcell;
1090
        }
1091
 
1092
        // Set the table body data.
1093
        foreach ($this->tabledata as $rowdata) {
1094
            $rowcells = [];
1095
            // Set a rowspan cell, if applicable.
1096
            if (isset($rowdata['leader'])) {
1097
                $rowspancell = new \html_table_cell('');
1098
                $rowspancell->attributes['class'] = $rowdata['leader']['class'];
1099
                $rowspancell->rowspan = $rowdata['leader']['rowspan'];
1100
                $rowcells[] = $rowspancell;
1101
            }
1102
 
1103
            // Set the row cells.
1104
            foreach ($this->tablecolumns as $tablecolumn) {
1105
                $content = $rowdata[$tablecolumn]['content'] ?? null;
1106
 
1107
                if (!is_null($content)) {
1108
                    $rowcell = new \html_table_cell($content);
1109
 
1110
                    // Grade item names and cateogry names are referenced in the `headers` attribute of table cells.
1111
                    // These table cells should be set to <th> tags.
1112
                    if ($tablecolumn === 'itemname') {
1113
                        $rowcell->header = true;
1114
                    }
1115
 
1116
                    if (isset($rowdata[$tablecolumn]['class'])) {
1117
                        $rowcell->attributes['class'] = $rowdata[$tablecolumn]['class'];
1118
                    }
1119
                    if (isset($rowdata[$tablecolumn]['colspan'])) {
1120
                        $rowcell->colspan = $rowdata[$tablecolumn]['colspan'];
1121
                    }
1122
                    if (isset($rowdata[$tablecolumn]['id'])) {
1123
                        $rowcell->id = $rowdata[$tablecolumn]['id'];
1124
                    }
1125
                    if (isset($rowdata[$tablecolumn]['headers'])) {
1126
                        $rowcell->attributes['headers'] = $rowdata[$tablecolumn]['headers'];
1127
                    }
1128
                    $rowcells[] = $rowcell;
1129
                }
1130
            }
1131
 
1132
            $tablerow = new \html_table_row($rowcells);
1133
            // Generate classes which will be attributed to the current row and will be used to identify all parent
1134
            // categories of this grading item or a category (e.g. 'cat_2 cat_5'). These classes are utilized by the
1135
            // category toggle (expand/collapse) functionality.
1136
            $classes = implode(" ", array_map(function($parentcategoryid) {
1137
                return "cat_{$parentcategoryid}";
1138
            }, $rowdata['parentcategories']));
1139
 
1140
            $classes .= isset($rowdata['spacer']) && $rowdata['spacer'] ? ' spacer' : '';
1141
 
1142
            $tablerow->attributes = ['class' => $classes, 'data-hidden' => 'false'];
1143
            $table->data[] = $tablerow;
1144
        }
1145
 
1146
        $userreporttable = \html_writer::table($table);
1147
        $PAGE->requires->js_call_amd('gradereport_user/gradecategorytoggle', 'init', ["user-report-{$this->user->id}"]);
1148
 
1149
        if ($return) {
1150
            return \html_writer::div($userreporttable, 'user-report-container', ['id' => "user-report-{$this->user->id}"]);
1151
        }
1152
 
1153
        echo \html_writer::div($userreporttable, 'user-report-container', ['id' => "user-report-{$this->user->id}"]);
1154
    }
1155
 
1156
    /**
1157
     * Processes the data sent by the form (grades and feedbacks).
1158
     *
1159
     * @param array $data Take in some data to provide to the base function.
1160
     * @return void Success or Failure (array of errors).
1161
     */
1162
    public function process_data($data): void {
1163
    }
1164
 
1165
    /**
1166
     * Stub function.
1167
     *
1168
     * @param string $target
1169
     * @param string $action
1170
     * @return void
1171
     */
1172
    public function process_action($target, $action): void {
1173
    }
1174
 
1175
    /**
1176
     * Build the html for the zero state of the user report.
1177
     * @return string HTML to display
1178
     */
1179
    public function output_report_zerostate(): string {
1180
        global $OUTPUT;
1181
 
1182
        $context = [
1183
            'imglink' => $OUTPUT->image_url('zero_state', 'gradereport_user'),
1184
        ];
1185
        return $OUTPUT->render_from_template('gradereport_user/zero_state', $context);
1186
    }
1187
 
1188
    /**
1189
     * Trigger the grade_report_viewed event
1190
     *
1191
     * @since Moodle 2.9
1192
     */
1193
    public function viewed() {
1194
        $event = \gradereport_user\event\grade_report_viewed::create(
1195
            [
1196
                'context' => $this->context,
1197
                'courseid' => $this->courseid,
1198
                'relateduserid' => $this->user->id,
1199
            ]
1200
        );
1201
        $event->trigger();
1202
    }
1203
}