Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

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