Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

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'] = '&nbsp;';
 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  }