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