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