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 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [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  /**
  18   * File containing the grade_report class
  19   *
  20   * @package   core_grades
  21   * @copyright 2007 Moodle Pty Ltd (http://moodle.com)
  22   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  use core_user\fields;
  26  
  27  require_once($CFG->libdir.'/gradelib.php');
  28  
  29  /**
  30   * An abstract class containing variables and methods used by all or most reports.
  31   * @copyright 2007 Moodle Pty Ltd (http://moodle.com)
  32   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  33   */
  34  abstract class grade_report {
  35      /**
  36       * The courseid.
  37       * @var int $courseid
  38       */
  39      public $courseid;
  40  
  41      /**
  42       * The course.
  43       * @var object $course
  44       */
  45      public $course;
  46  
  47      /** Grade plugin return tracking object.
  48       * @var object $gpr
  49       */
  50      public $gpr;
  51  
  52      /**
  53       * The context.
  54       *
  55       * @var context $context
  56       */
  57      public $context;
  58  
  59      /**
  60       * The grade_tree object.
  61       * @var grade_tree $gtree
  62       */
  63      public $gtree;
  64  
  65      /**
  66       * User preferences related to this report.
  67       * @var array $prefs
  68       */
  69      public $prefs = array();
  70  
  71      /**
  72       * The roles for this report.
  73       * @var string $gradebookroles
  74       */
  75      public $gradebookroles;
  76  
  77      /**
  78       * base url for sorting by first/last name.
  79       * @var string $baseurl
  80       */
  81      public $baseurl;
  82  
  83      /**
  84       * base url for paging.
  85       * @var string $pbarurl
  86       */
  87      public $pbarurl;
  88  
  89      /**
  90       * Current page (for paging).
  91       * @var int $page
  92       */
  93      public $page;
  94  
  95      // GROUP VARIABLES (including SQL)
  96  
  97      /**
  98       * The current group being displayed.
  99       * @var int $currentgroup
 100       */
 101      public $currentgroup;
 102  
 103      /**
 104       * The current groupname being displayed.
 105       * @var string $currentgroupname
 106       */
 107      public $currentgroupname;
 108  
 109      /**
 110       * Current course group mode
 111       * @var int $groupmode
 112       */
 113      public $groupmode;
 114  
 115      /**
 116       * A HTML select element used to select the current group.
 117       * @var string $group_selector
 118       */
 119      public $group_selector;
 120  
 121      /**
 122       * An SQL fragment used to add linking information to the group tables.
 123       * @var string $groupsql
 124       */
 125      protected $groupsql;
 126  
 127      /**
 128       * An SQL constraint to append to the queries used by this object to build the report.
 129       * @var string $groupwheresql
 130       */
 131      protected $groupwheresql;
 132  
 133      /**
 134       * The ordered params for $groupwheresql
 135       * @var array $groupwheresql_params
 136       */
 137      protected $groupwheresql_params = array();
 138  
 139      // USER VARIABLES (including SQL).
 140  
 141      /**
 142       * An SQL constraint to append to the queries used by this object to build the report.
 143       * @var string $userwheresql
 144       */
 145      protected $userwheresql;
 146  
 147      /**
 148       * The ordered params for $userwheresql
 149       * @var array $userwheresql_params
 150       */
 151      protected $userwheresql_params = array();
 152  
 153      /**
 154       * To store user data
 155       * @var stdClass $user
 156       */
 157      public $user;
 158  
 159      /**
 160       * show course/category totals if they contain hidden items
 161       * @var array $showtotalsifcontainhidden
 162       */
 163      public $showtotalsifcontainhidden = [];
 164  
 165      /**
 166       * To store a link to preferences page
 167       * @var string $preferences_page
 168       */
 169      protected $preferences_page;
 170  
 171      /**
 172       * If the user is wanting to search for a particular user within searchable fields their needle will be placed here.
 173       * @var string $usersearch
 174       */
 175      protected string $usersearch = '';
 176  
 177      /**
 178       * If the user is wanting to show only one particular user their id will be placed here.
 179       * @var int $userid
 180       */
 181      protected int $userid = -1;
 182  
 183      /**
 184       * Constructor. Sets local copies of user preferences and initialises grade_tree.
 185       * @param int $courseid
 186       * @param object $gpr grade plugin return tracking object
 187       * @param string $context
 188       * @param int $page The current page being viewed (when report is paged)
 189       */
 190      public function __construct($courseid, $gpr, $context, $page=null) {
 191          global $CFG, $COURSE, $DB;
 192  
 193          if (empty($CFG->gradebookroles)) {
 194              throw new \moodle_exception('norolesdefined', 'grades');
 195          }
 196  
 197          $this->courseid  = $courseid;
 198          if ($this->courseid == $COURSE->id) {
 199              $this->course = $COURSE;
 200          } else {
 201              $this->course = $DB->get_record('course', array('id' => $this->courseid));
 202          }
 203  
 204          $this->gpr       = $gpr;
 205          $this->context   = $context;
 206          $this->page      = $page;
 207  
 208          // roles to be displayed in the gradebook
 209          $this->gradebookroles = $CFG->gradebookroles;
 210  
 211          // Set up link to preferences page
 212          $this->preferences_page = $CFG->wwwroot.'/grade/report/grader/preferences.php?id='.$courseid;
 213  
 214          // init gtree in child class
 215  
 216          // Set any url params.
 217          $this->usersearch = optional_param('gpr_search', '', PARAM_NOTAGS);
 218          $this->userid = optional_param('gpr_userid', -1, PARAM_INT);
 219      }
 220  
 221      /**
 222       * Given the name of a user preference (without grade_report_ prefix), locally saves then returns
 223       * the value of that preference. If the preference has already been fetched before,
 224       * the saved value is returned. If the preference is not set at the User level, the $CFG equivalent
 225       * is given (site default).
 226       * Can be called statically, but then doesn't benefit from caching
 227       * @param string $pref The name of the preference (do not include the grade_report_ prefix)
 228       * @param int $objectid An optional itemid or categoryid to check for a more fine-grained preference
 229       * @return mixed The value of the preference
 230       */
 231      public function get_pref($pref, $objectid=null) {
 232          global $CFG;
 233          $fullprefname = 'grade_report_' . $pref;
 234          $shortprefname = 'grade_' . $pref;
 235  
 236          $retval = null;
 237  
 238          if (!isset($this) OR get_class($this) != 'grade_report') {
 239              if (!empty($objectid)) {
 240                  $retval = get_user_preferences($fullprefname . $objectid, self::get_pref($pref));
 241              } else if (isset($CFG->$fullprefname)) {
 242                  $retval = get_user_preferences($fullprefname, $CFG->$fullprefname);
 243              } else if (isset($CFG->$shortprefname)) {
 244                  $retval = get_user_preferences($fullprefname, $CFG->$shortprefname);
 245              } else {
 246                  $retval = null;
 247              }
 248          } else {
 249              if (empty($this->prefs[$pref.$objectid])) {
 250  
 251                  if (!empty($objectid)) {
 252                      $retval = get_user_preferences($fullprefname . $objectid);
 253                      if (empty($retval)) {
 254                          // No item pref found, we are returning the global preference
 255                          $retval = $this->get_pref($pref);
 256                          $objectid = null;
 257                      }
 258                  } else {
 259                      $retval = get_user_preferences($fullprefname, $CFG->$fullprefname);
 260                  }
 261                  $this->prefs[$pref.$objectid] = $retval;
 262              } else {
 263                  $retval = $this->prefs[$pref.$objectid];
 264              }
 265          }
 266  
 267          return $retval;
 268      }
 269  
 270      /**
 271       * Uses set_user_preferences() to update the value of a user preference. If 'default' is given as the value,
 272       * the preference will be removed in favour of a higher-level preference.
 273       * @param string $pref The name of the preference.
 274       * @param mixed $pref_value The value of the preference.
 275       * @param int $itemid An optional itemid to which the preference will be assigned
 276       * @return bool Success or failure.
 277       */
 278      public function set_pref($pref, $pref_value='default', $itemid=null) {
 279          $fullprefname = 'grade_report_' . $pref;
 280          if ($pref_value == 'default') {
 281              return unset_user_preference($fullprefname.$itemid);
 282          } else {
 283              return set_user_preference($fullprefname.$itemid, $pref_value);
 284          }
 285      }
 286  
 287      /**
 288       * Handles form data sent by this report for this report. Abstract method to implement in all children.
 289       * @abstract
 290       * @param array $data
 291       * @return mixed True or array of errors
 292       */
 293      abstract public function process_data($data);
 294  
 295      /**
 296       * Processes a single action against a category, grade_item or grade.
 297       * @param string $target Sortorder
 298       * @param string $action Which action to take (edit, delete etc...)
 299       * @return
 300       */
 301      abstract public function process_action($target, $action);
 302  
 303      /**
 304       * Add additional links specific to plugin
 305       * @param context_course $context Course context
 306       * @param int $courseid Course ID
 307       * @param array  $element An array representing an element in the grade_tree
 308       * @param grade_plugin_return $gpr A grade_plugin_return object
 309       * @param string $mode Mode (user or grade item)
 310       * @param stdClass $templatecontext Template context
 311       * @param bool $otherplugins If we need to insert links to other plugins
 312       * @return ?stdClass Updated template context
 313       */
 314      public static function get_additional_context(context_course $context, int $courseid, array $element,
 315              grade_plugin_return $gpr, string $mode, stdClass $templatecontext, bool $otherplugins = false): ?stdClass {
 316  
 317          if (!$otherplugins) {
 318              $component = 'gradereport_' . $gpr->plugin;
 319              $params = [$context, $courseid, $element, $gpr, $mode, $templatecontext];
 320              return component_callback($component, 'get_report_link', $params);
 321          } else {
 322              // Loop through all installed grade reports.
 323              foreach (core_component::get_plugin_list('gradereport') as $plugin => $plugindir) {
 324                  $params = [$context, $courseid, $element, $gpr, $mode, $templatecontext];
 325                  $component = 'gradereport_' . $plugin;
 326                  $templatecontextupdated = component_callback($component, 'get_report_link', $params);
 327                  if ($templatecontextupdated) {
 328                      $templatecontext = $templatecontextupdated;
 329                  }
 330              }
 331              return $templatecontext;
 332          }
 333      }
 334  
 335      /**
 336       * First checks the cached language strings, then returns match if found, or uses get_string()
 337       * to get it from the DB, caches it then returns it.
 338       *
 339       * @deprecated since 4.2
 340       * @todo MDL-77307 This will be deleted in Moodle 4.6.
 341       * @param string $strcode
 342       * @param string $section Optional language section
 343       * @return string
 344       */
 345      public function get_lang_string($strcode, $section=null) {
 346          debugging('grade_report::get_lang_string() is deprecated, please use' .
 347              ' get_string() instead.', DEBUG_DEVELOPER);
 348  
 349          if (empty($this->lang_strings[$strcode])) {
 350              $this->lang_strings[$strcode] = get_string($strcode, $section);
 351          }
 352          return $this->lang_strings[$strcode];
 353      }
 354  
 355      /**
 356       * Fetches and returns a count of all the users that will be shown on this page.
 357       * @param boolean $groups include groups limit
 358       * @param boolean $users include users limit - default false, used for searching purposes
 359       * @return int Count of users
 360       */
 361      public function get_numusers($groups = true, $users = false) {
 362          global $CFG, $DB;
 363          $userwheresql = "";
 364          $groupsql      = "";
 365          $groupwheresql = "";
 366  
 367          // Limit to users with a gradeable role.
 368          list($gradebookrolessql, $gradebookrolesparams) = $DB->get_in_or_equal(explode(',', $this->gradebookroles), SQL_PARAMS_NAMED, 'grbr0');
 369  
 370          // Limit to users with an active enrollment.
 371          list($enrolledsql, $enrolledparams) = get_enrolled_sql($this->context);
 372  
 373          // We want to query both the current context and parent contexts.
 374          list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($this->context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx');
 375  
 376          $params = array_merge($gradebookrolesparams, $enrolledparams, $relatedctxparams);
 377  
 378          if ($users) {
 379              $userwheresql = $this->userwheresql;
 380              $params       = array_merge($params, $this->userwheresql_params);
 381          }
 382  
 383          if ($groups) {
 384              $groupsql      = $this->groupsql;
 385              $groupwheresql = $this->groupwheresql;
 386              $params        = array_merge($params, $this->groupwheresql_params);
 387          }
 388  
 389          $sql = "SELECT DISTINCT u.id
 390                         FROM {user} u
 391                         JOIN ($enrolledsql) je
 392                              ON je.id = u.id
 393                         JOIN {role_assignments} ra
 394                              ON u.id = ra.userid
 395                         $groupsql
 396                        WHERE ra.roleid $gradebookrolessql
 397                              AND u.deleted = 0
 398                              $userwheresql
 399                              $groupwheresql
 400                              AND ra.contextid $relatedctxsql";
 401          $selectedusers = $DB->get_records_sql($sql, $params);
 402  
 403          $count = 0;
 404          // Check if user's enrolment is active and should be displayed.
 405          if (!empty($selectedusers)) {
 406              $coursecontext = $this->context->get_course_context(true);
 407  
 408              $defaultgradeshowactiveenrol = !empty($CFG->grade_report_showonlyactiveenrol);
 409              $showonlyactiveenrol = get_user_preferences('grade_report_showonlyactiveenrol', $defaultgradeshowactiveenrol);
 410              $showonlyactiveenrol = $showonlyactiveenrol || !has_capability('moodle/course:viewsuspendedusers', $coursecontext);
 411  
 412              if ($showonlyactiveenrol) {
 413                  $useractiveenrolments = get_enrolled_users($coursecontext, '', 0, 'u.id',  null, 0, 0, true);
 414              }
 415  
 416              foreach ($selectedusers as $id => $value) {
 417                  if (!$showonlyactiveenrol || ($showonlyactiveenrol && array_key_exists($id, $useractiveenrolments))) {
 418                      $count++;
 419                  }
 420              }
 421          }
 422          return $count;
 423      }
 424  
 425      /**
 426       * Shows support for being used as a 'Grades' report.
 427       */
 428      public static function supports_mygrades() {
 429          return false;
 430      }
 431  
 432      /**
 433       * Sets up this object's group variables, mainly to restrict the selection of users to display.
 434       */
 435      protected function setup_groups() {
 436          // find out current groups mode
 437          if ($this->groupmode = groups_get_course_groupmode($this->course)) {
 438              if (empty($this->gpr->groupid)) {
 439                  $this->currentgroup = groups_get_course_group($this->course, true);
 440              } else {
 441                  $this->currentgroup = $this->gpr->groupid;
 442              }
 443              $this->group_selector = groups_print_course_menu($this->course, $this->pbarurl, true);
 444  
 445              if ($this->groupmode == SEPARATEGROUPS and !$this->currentgroup and !has_capability('moodle/site:accessallgroups', $this->context)) {
 446                  $this->currentgroup = -2; // means can not access any groups at all
 447              }
 448              if ($this->currentgroup) {
 449                  if ($group = groups_get_group($this->currentgroup)) {
 450                      $this->currentgroupname = $group->name;
 451                  }
 452                  $this->groupsql             = " JOIN {groups_members} gm ON gm.userid = u.id ";
 453                  $this->groupwheresql        = " AND gm.groupid = :gr_grpid ";
 454                  $this->groupwheresql_params = array('gr_grpid'=>$this->currentgroup);
 455              }
 456          }
 457      }
 458  
 459      /**
 460       * Sets up this report's user criteria to restrict the selection of users to display.
 461       */
 462      public function setup_users() {
 463          global $SESSION, $DB;
 464  
 465          $filterfirstnamekey = "filterfirstname-{$this->context->id}";
 466          $filtersurnamekey = "filtersurname-{$this->context->id}";
 467  
 468          $this->userwheresql = "";
 469          $this->userwheresql_params = array();
 470          if (!empty($SESSION->gradereport[$filterfirstnamekey])) {
 471              $this->userwheresql .= ' AND '.$DB->sql_like('u.firstname', ':firstname', false, false);
 472              $this->userwheresql_params['firstname'] = $SESSION->gradereport[$filterfirstnamekey] . '%';
 473          }
 474          if (!empty($SESSION->gradereport[$filtersurnamekey])) {
 475              $this->userwheresql .= ' AND '.$DB->sql_like('u.lastname', ':lastname', false, false);
 476              $this->userwheresql_params['lastname'] = $SESSION->gradereport[$filtersurnamekey] . '%';
 477          }
 478  
 479          // When a user wants to view a particular user rather than a set of users.
 480          // By omission when selecting one user, also allow passing the search value around.
 481          if ($this->userid !== -1) {
 482              $this->userwheresql .= " AND u.id = :uid";
 483              $this->userwheresql_params['uid'] = $this->userid;
 484          }
 485  
 486          // A user wants to return a subset of learners that match their search criteria.
 487          if ($this->usersearch !== '' && $this->userid === -1) {
 488              // Get the fields for all contexts because there is a special case later where it allows
 489              // matches of fields you can't access if they are on your own account.
 490              $userfields = fields::for_identity(null, false)->with_userpic();
 491              ['mappings' => $mappings]  = (array)$userfields->get_sql('u', true);
 492              [
 493                  'where' => $keywordswhere,
 494                  'params' => $keywordsparams,
 495              ] = $this->get_users_search_sql($mappings, (array)$userfields);
 496              $this->userwheresql .= " AND $keywordswhere";
 497              $this->userwheresql_params = array_merge($this->userwheresql_params, $keywordsparams);
 498          }
 499      }
 500  
 501      /**
 502       * Prepare SQL where clause and associated parameters for any user searching being performed.
 503       * This mostly came from core_user\table\participants_search with some slight modifications four our use case.
 504       *
 505       * @param array $mappings Array of field mappings (fieldname => SQL code for the value)
 506       * @param array $userfields An array that we cast from user profile fields to search within.
 507       * @return array SQL query data in the format ['where' => '', 'params' => []].
 508       */
 509      protected function get_users_search_sql(array $mappings, array $userfields): array {
 510          global $DB, $USER;
 511  
 512          $canviewfullnames = has_capability('moodle/site:viewfullnames', $this->context);
 513  
 514          $params = [];
 515          $searchkey1 = 'search01';
 516          $searchkey2 = 'search02';
 517          $searchkey3 = 'search03';
 518  
 519          $conditions = [];
 520  
 521          // Search by fullname.
 522          [$fullname, $fullnameparams] = fields::get_sql_fullname('u', $canviewfullnames);
 523          $conditions[] = $DB->sql_like($fullname, ':' . $searchkey1, false, false);
 524          $params = array_merge($params, $fullnameparams);
 525  
 526          // Search by email.
 527          $email = $DB->sql_like('email', ':' . $searchkey2, false, false);
 528  
 529          if (!in_array('email', $userfields)) {
 530              $maildisplay = 'maildisplay0';
 531              $userid1 = 'userid01';
 532              // Prevent users who hide their email address from being found by others
 533              // who aren't allowed to see hidden email addresses.
 534              $email = "(". $email ." AND (" .
 535                  "u.maildisplay <> :$maildisplay " .
 536                  "OR u.id = :$userid1". // Users can always find themselves.
 537                  "))";
 538              $params[$maildisplay] = core_user::MAILDISPLAY_HIDE;
 539              $params[$userid1] = $USER->id;
 540          }
 541  
 542          $conditions[] = $email;
 543  
 544          // Search by idnumber.
 545          $idnumber = $DB->sql_like('idnumber', ':' . $searchkey3, false, false);
 546  
 547          if (!in_array('idnumber', $userfields)) {
 548              $userid2 = 'userid02';
 549              // Users who aren't allowed to see idnumbers should at most find themselves
 550              // when searching for an idnumber.
 551              $idnumber = "(". $idnumber . " AND u.id = :$userid2)";
 552              $params[$userid2] = $USER->id;
 553          }
 554  
 555          $conditions[] = $idnumber;
 556  
 557          // Search all user identify fields.
 558          $extrasearchfields = fields::get_identity_fields(null, false);
 559          foreach ($extrasearchfields as $fieldindex => $extrasearchfield) {
 560              if (in_array($extrasearchfield, ['email', 'idnumber', 'country'])) {
 561                  // Already covered above.
 562                  continue;
 563              }
 564              // The param must be short (max 32 characters) so don't include field name.
 565              $param = $searchkey3 . '_ident' . $fieldindex;
 566              $fieldsql = $mappings[$extrasearchfield];
 567              $condition = $DB->sql_like($fieldsql, ':' . $param, false, false);
 568              $params[$param] = "%$this->usersearch%";
 569  
 570              if (!in_array($extrasearchfield, $userfields)) {
 571                  // User cannot see this field, but allow match if their own account.
 572                  $userid3 = 'userid03_ident' . $fieldindex;
 573                  $condition = "(". $condition . " AND u.id = :$userid3)";
 574                  $params[$userid3] = $USER->id;
 575              }
 576              $conditions[] = $condition;
 577          }
 578  
 579          $where = "(". implode(" OR ", $conditions) .") ";
 580          $params[$searchkey1] = "%$this->usersearch%";
 581          $params[$searchkey2] = "%$this->usersearch%";
 582          $params[$searchkey3] = "%$this->usersearch%";
 583  
 584          return [
 585              'where' => $where,
 586              'params' => $params,
 587          ];
 588      }
 589  
 590      /**
 591       * Returns an arrow icon inside an <a> tag, for the purpose of sorting a column.
 592       * @param string $direction
 593       * @param moodle_url|null $sortlink
 594       */
 595      protected function get_sort_arrow(string $direction = 'down', ?moodle_url $sortlink = null) {
 596          global $OUTPUT;
 597          $pix = ['up' => 't/sort_desc', 'down' => 't/sort_asc'];
 598          $matrix = ['up' => 'desc', 'down' => 'asc'];
 599          $strsort = get_string($matrix[$direction], 'moodle');
 600          $arrow = $OUTPUT->pix_icon($pix[$direction], '', '', ['class' => 'sorticon']);
 601  
 602          if (!empty($sortlink)) {
 603              $sortlink->param('sort', ($direction == 'up' ? 'asc' : 'desc'));
 604          }
 605  
 606          return html_writer::link($sortlink, $arrow, ['title' => $strsort, 'aria-label' => $strsort, 'data-collapse' => 'sort',
 607              'class' => 'arrow_link py-1']);
 608      }
 609  
 610      /**
 611       * Optionally blank out course/category totals if they contain any hidden items
 612       * @param string $courseid the course id
 613       * @param string $course_item an instance of grade_item
 614       * @param string $finalgrade the grade for the course_item
 615       * @return array[] containing values for 'grade', 'grademax', 'grademin', 'aggregationstatus' and 'aggregationweight'
 616       */
 617      protected function blank_hidden_total_and_adjust_bounds($courseid, $course_item, $finalgrade) {
 618          global $CFG, $DB;
 619          static $hiding_affected = null;//array of items in this course affected by hiding
 620  
 621          // If we're dealing with multiple users we need to know when we've moved on to a new user.
 622          static $previous_userid = null;
 623  
 624          // If we're dealing with multiple courses we need to know when we've moved on to a new course.
 625          static $previous_courseid = null;
 626  
 627          $coursegradegrade = grade_grade::fetch(array('userid'=>$this->user->id, 'itemid'=>$course_item->id));
 628          $grademin = $course_item->grademin;
 629          $grademax = $course_item->grademax;
 630          if ($coursegradegrade) {
 631              $grademin = $coursegradegrade->get_grade_min();
 632              $grademax = $coursegradegrade->get_grade_max();
 633          } else {
 634              $coursegradegrade = new grade_grade(array('userid'=>$this->user->id, 'itemid'=>$course_item->id), false);
 635          }
 636          $hint = $coursegradegrade->get_aggregation_hint();
 637          $aggregationstatus = $hint['status'];
 638          $aggregationweight = $hint['weight'];
 639  
 640          if (!is_array($this->showtotalsifcontainhidden)) {
 641              debugging('showtotalsifcontainhidden should be an array', DEBUG_DEVELOPER);
 642              $this->showtotalsifcontainhidden = array($courseid => $this->showtotalsifcontainhidden);
 643          }
 644  
 645          if ($this->showtotalsifcontainhidden[$courseid] == GRADE_REPORT_SHOW_REAL_TOTAL_IF_CONTAINS_HIDDEN) {
 646              return array('grade' => $finalgrade,
 647                           'grademin' => $grademin,
 648                           'grademax' => $grademax,
 649                           'aggregationstatus' => $aggregationstatus,
 650                           'aggregationweight' => $aggregationweight);
 651          }
 652  
 653          // If we've moved on to another course or user, reload the grades.
 654          if ($previous_userid != $this->user->id || $previous_courseid != $courseid) {
 655              $hiding_affected = null;
 656              $previous_userid = $this->user->id;
 657              $previous_courseid = $courseid;
 658          }
 659  
 660          if (!$hiding_affected) {
 661              $items = grade_item::fetch_all(array('courseid'=>$courseid));
 662              $grades = array();
 663              $sql = "SELECT g.*
 664                        FROM {grade_grades} g
 665                        JOIN {grade_items} gi ON gi.id = g.itemid
 666                       WHERE g.userid = {$this->user->id} AND gi.courseid = {$courseid}";
 667              if ($gradesrecords = $DB->get_records_sql($sql)) {
 668                  foreach ($gradesrecords as $grade) {
 669                      $grades[$grade->itemid] = new grade_grade($grade, false);
 670                  }
 671                  unset($gradesrecords);
 672              }
 673              foreach ($items as $itemid => $unused) {
 674                  if (!isset($grades[$itemid])) {
 675                      $grade_grade = new grade_grade();
 676                      $grade_grade->userid = $this->user->id;
 677                      $grade_grade->itemid = $items[$itemid]->id;
 678                      $grades[$itemid] = $grade_grade;
 679                  }
 680                  $grades[$itemid]->grade_item =& $items[$itemid];
 681              }
 682              $hiding_affected = grade_grade::get_hiding_affected($grades, $items);
 683          }
 684  
 685          //if the item definitely depends on a hidden item
 686          if (array_key_exists($course_item->id, $hiding_affected['altered']) ||
 687                  array_key_exists($course_item->id, $hiding_affected['alteredgrademin']) ||
 688                  array_key_exists($course_item->id, $hiding_affected['alteredgrademax']) ||
 689                  array_key_exists($course_item->id, $hiding_affected['alteredaggregationstatus']) ||
 690                  array_key_exists($course_item->id, $hiding_affected['alteredaggregationweight'])) {
 691              if (!$this->showtotalsifcontainhidden[$courseid] && array_key_exists($course_item->id, $hiding_affected['altered'])) {
 692                  // Hide the grade, but only when it has changed.
 693                  $finalgrade = null;
 694              } else {
 695                  //use reprocessed marks that exclude hidden items
 696                  if (array_key_exists($course_item->id, $hiding_affected['altered'])) {
 697                      $finalgrade = $hiding_affected['altered'][$course_item->id];
 698                  }
 699                  if (array_key_exists($course_item->id, $hiding_affected['alteredgrademin'])) {
 700                      $grademin = $hiding_affected['alteredgrademin'][$course_item->id];
 701                  }
 702                  if (array_key_exists($course_item->id, $hiding_affected['alteredgrademax'])) {
 703                      $grademax = $hiding_affected['alteredgrademax'][$course_item->id];
 704                  }
 705                  if (array_key_exists($course_item->id, $hiding_affected['alteredaggregationstatus'])) {
 706                      $aggregationstatus = $hiding_affected['alteredaggregationstatus'][$course_item->id];
 707                  }
 708                  if (array_key_exists($course_item->id, $hiding_affected['alteredaggregationweight'])) {
 709                      $aggregationweight = $hiding_affected['alteredaggregationweight'][$course_item->id];
 710                  }
 711  
 712                  if (!$this->showtotalsifcontainhidden[$courseid]) {
 713                      // If the course total is hidden we must hide the weight otherwise
 714                      // it can be used to compute the course total.
 715                      $aggregationstatus = 'unknown';
 716                      $aggregationweight = null;
 717                  }
 718              }
 719          } else if (array_key_exists($course_item->id, $hiding_affected['unknowngrades'])) {
 720              //not sure whether or not this item depends on a hidden item
 721              if (!$this->showtotalsifcontainhidden[$courseid]) {
 722                  //hide the grade
 723                  $finalgrade = null;
 724              } else {
 725                  //use reprocessed marks that exclude hidden items
 726                  $finalgrade = $hiding_affected['unknowngrades'][$course_item->id];
 727  
 728                  if (array_key_exists($course_item->id, $hiding_affected['alteredgrademin'])) {
 729                      $grademin = $hiding_affected['alteredgrademin'][$course_item->id];
 730                  }
 731                  if (array_key_exists($course_item->id, $hiding_affected['alteredgrademax'])) {
 732                      $grademax = $hiding_affected['alteredgrademax'][$course_item->id];
 733                  }
 734                  if (array_key_exists($course_item->id, $hiding_affected['alteredaggregationstatus'])) {
 735                      $aggregationstatus = $hiding_affected['alteredaggregationstatus'][$course_item->id];
 736                  }
 737                  if (array_key_exists($course_item->id, $hiding_affected['alteredaggregationweight'])) {
 738                      $aggregationweight = $hiding_affected['alteredaggregationweight'][$course_item->id];
 739                  }
 740              }
 741          }
 742  
 743          return array('grade' => $finalgrade, 'grademin' => $grademin, 'grademax' => $grademax, 'aggregationstatus'=>$aggregationstatus, 'aggregationweight'=>$aggregationweight);
 744      }
 745  
 746      /**
 747       * Optionally blank out course/category totals if they contain any hidden items
 748       * @deprecated since Moodle 2.8 - Call blank_hidden_total_and_adjust_bounds instead.
 749       * @param string $courseid the course id
 750       * @param string $course_item an instance of grade_item
 751       * @param string $finalgrade the grade for the course_item
 752       * @return string The new final grade
 753       */
 754      protected function blank_hidden_total($courseid, $course_item, $finalgrade) {
 755          // Note it is flawed to call this function directly because
 756          // the aggregated grade does not make sense without the updated min and max information.
 757  
 758          debugging('grade_report::blank_hidden_total() is deprecated.
 759                     Call grade_report::blank_hidden_total_and_adjust_bounds instead.', DEBUG_DEVELOPER);
 760          $result = $this->blank_hidden_total_and_adjust_bounds($courseid, $course_item, $finalgrade);
 761          return $result['grade'];
 762      }
 763  
 764      /**
 765       * Calculate average grade for a given grade item.
 766       * Based on calculate_averages function from grade/report/user/lib.php
 767       *
 768       * @param grade_item $gradeitem Grade item
 769       * @param array $info Ungraded grade items counts and report preferences.
 770       * @return array Average grade and meancount.
 771       */
 772      public static function calculate_average(grade_item $gradeitem, array $info): array {
 773  
 774          $meanselection = $info['report']['meanselection'];
 775          $totalcount = $info['report']['totalcount'];
 776          $ungradedcounts = $info['ungradedcounts'];
 777          $sumarray = $info['sumarray'];
 778  
 779          if (empty($sumarray[$gradeitem->id])) {
 780              $sumarray[$gradeitem->id] = 0;
 781          }
 782  
 783          if (empty($ungradedcounts[$gradeitem->id])) {
 784              $ungradedcounts = 0;
 785          } else {
 786              $ungradedcounts = $ungradedcounts[$gradeitem->id]->count;
 787          }
 788  
 789          // If they want the averages to include all grade items.
 790          if ($meanselection == GRADE_REPORT_MEAN_GRADED) {
 791              $meancount = $totalcount - $ungradedcounts;
 792          } else {
 793              // Bump up the sum by the number of ungraded items * grademin.
 794              $sumarray[$gradeitem->id] += ($ungradedcounts * $gradeitem->grademin);
 795              $meancount = $totalcount;
 796          }
 797  
 798          $aggr['meancount'] = $meancount;
 799  
 800          if (empty($sumarray[$gradeitem->id]) || $meancount == 0) {
 801              $aggr['average'] = null;
 802          } else {
 803              $sum = $sumarray[$gradeitem->id];
 804              $aggr['average'] = $sum / $meancount;
 805          }
 806          return $aggr;
 807      }
 808  
 809      /**
 810       * Get ungraded grade items info and sum of all grade items in a course.
 811       * Based on calculate_averages function from grade/report/user/lib.php
 812       *
 813       * @return array Ungraded grade items counts with report preferences.
 814       */
 815      public function ungraded_counts(): array {
 816          global $DB;
 817  
 818          $groupid = null;
 819          if (isset($this->gpr->groupid)) {
 820              $groupid = $this->gpr->groupid;
 821          }
 822  
 823          $info = [];
 824          $info['report'] = [
 825              'averagesdisplaytype' => $this->get_pref('averagesdisplaytype'),
 826              'averagesdecimalpoints' => $this->get_pref('averagesdecimalpoints'),
 827              'meanselection' => $this->get_pref('meanselection'),
 828              'shownumberofgrades' => $this->get_pref('shownumberofgrades'),
 829              'totalcount' => $this->get_numusers(!is_null($groupid)),
 830          ];
 831  
 832          // We want to query both the current context and parent contexts.
 833          list($relatedctxsql, $relatedctxparams) =
 834              $DB->get_in_or_equal($this->context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx');
 835  
 836          // Limit to users with a gradeable role ie students.
 837          list($gradebookrolessql, $gradebookrolesparams) =
 838              $DB->get_in_or_equal(explode(',', $this->gradebookroles), SQL_PARAMS_NAMED, 'grbr0');
 839  
 840          // Limit to users with an active enrolment.
 841          $defaultgradeshowactiveenrol = !empty($CFG->grade_report_showonlyactiveenrol);
 842          $showonlyactiveenrol = get_user_preferences('grade_report_showonlyactiveenrol', $defaultgradeshowactiveenrol);
 843          $showonlyactiveenrol = $showonlyactiveenrol ||
 844              !has_capability('moodle/course:viewsuspendedusers', $this->context);
 845          list($enrolledsql, $enrolledparams) = get_enrolled_sql($this->context, '', 0, $showonlyactiveenrol);
 846  
 847          $params = array_merge($this->groupwheresql_params, $gradebookrolesparams, $enrolledparams, $relatedctxparams);
 848          $params['courseid'] = $this->courseid;
 849  
 850          // Aggregate on whole course only.
 851          if (empty($groupid)) {
 852              $this->groupsql = null;
 853              $this->groupwheresql = null;
 854          }
 855  
 856          // Empty grades must be evaluated as grademin, NOT always 0.
 857          // This query returns a count of ungraded grades (NULL finalgrade OR no matching record in grade_grades table).
 858          // No join condition when joining grade_items and user to get a grade item row for every user.
 859          // Then left join with grade_grades and look for rows with null final grade
 860          // (which includes grade items with no grade_grade).
 861          $sql = "SELECT gi.id, COUNT(u.id) AS count
 862                        FROM {grade_items} gi
 863                        JOIN {user} u ON u.deleted = 0
 864                        JOIN ($enrolledsql) je ON je.id = u.id
 865                        JOIN (
 866                                 SELECT DISTINCT ra.userid
 867                                   FROM {role_assignments} ra
 868                                  WHERE ra.roleid $gradebookrolessql
 869                                    AND ra.contextid $relatedctxsql
 870                             ) rainner ON rainner.userid = u.id
 871                        LEFT JOIN {grade_grades} gg
 872                               ON (gg.itemid = gi.id AND gg.userid = u.id AND gg.finalgrade IS NOT NULL AND gg.hidden = 0)
 873                        $this->groupsql
 874                       WHERE gi.courseid = :courseid
 875                             AND gg.finalgrade IS NULL
 876                             $this->groupwheresql
 877                    GROUP BY gi.id";
 878          $info['ungradedcounts'] = $DB->get_records_sql($sql, $params);
 879  
 880          // Find sums of all grade items in course.
 881          $sql = "SELECT gg.itemid, SUM(gg.finalgrade) AS sum
 882                        FROM {grade_items} gi
 883                        JOIN {grade_grades} gg ON gg.itemid = gi.id
 884                        JOIN {user} u ON u.id = gg.userid
 885                        JOIN ($enrolledsql) je ON je.id = gg.userid
 886                        JOIN (
 887                                     SELECT DISTINCT ra.userid
 888                                       FROM {role_assignments} ra
 889                                      WHERE ra.roleid $gradebookrolessql
 890                                        AND ra.contextid $relatedctxsql
 891                             ) rainner ON rainner.userid = u.id
 892                        $this->groupsql
 893                       WHERE gi.courseid = :courseid
 894                         AND u.deleted = 0
 895                         AND gg.finalgrade IS NOT NULL
 896                         AND gg.hidden = 0
 897                         $this->groupwheresql
 898                    GROUP BY gg.itemid";
 899  
 900          $sumarray = [];
 901          $sums = $DB->get_recordset_sql($sql, $params);
 902          foreach ($sums as $itemid => $csum) {
 903              $sumarray[$itemid] = grade_floatval($csum->sum);
 904          }
 905          $sums->close();
 906          $info['sumarray'] = $sumarray;
 907  
 908          return $info;
 909      }
 910  
 911      /**
 912       * Get grade item type names in a course to use in filter dropdown.
 913       *
 914       * @return array Item types.
 915       */
 916      public function item_types(): array {
 917          global $DB, $CFG;
 918  
 919          $modnames = [];
 920          $sql = "(SELECT gi.itemmodule
 921                     FROM {grade_items} gi
 922                    WHERE gi.courseid = :courseid1
 923                      AND gi.itemmodule IS NOT NULL)
 924                   UNION
 925                  (SELECT gi1.itemtype
 926                     FROM {grade_items} gi1
 927                    WHERE gi1.courseid = :courseid2
 928                      AND gi1.itemtype = 'manual')";
 929  
 930          $itemtypes = $DB->get_records_sql($sql, ['courseid1' => $this->courseid, 'courseid2' => $this->courseid]);
 931          foreach ($itemtypes as $itemtype => $value) {
 932              if (file_exists("$CFG->dirroot/mod/$itemtype/lib.php")) {
 933                  $modnames[$itemtype] = get_string("modulename", $itemtype, null, true);
 934              } else if ($itemtype == 'manual') {
 935                  $modnames[$itemtype] = get_string('manualitem', 'grades', null, true);
 936              }
 937          }
 938  
 939          return $modnames;
 940      }
 941  
 942      /**
 943       * Load a valid list of gradable users in a course.
 944       *
 945       * @param int $courseid The course ID.
 946       * @param int|null $groupid The group ID (optional).
 947       * @return array A list of enrolled gradable users.
 948       */
 949      public static function get_gradable_users(int $courseid, ?int $groupid = null): array {
 950          global $CFG;
 951          require_once($CFG->dirroot . '/grade/lib.php');
 952  
 953          $context = context_course::instance($courseid);
 954          $defaultgradeshowactiveenrol = !empty($CFG->grade_report_showonlyactiveenrol);
 955          $onlyactiveenrol = get_user_preferences('grade_report_showonlyactiveenrol', $defaultgradeshowactiveenrol) ||
 956              !has_capability('moodle/course:viewsuspendedusers', $context);
 957  
 958          return get_gradable_users($courseid, $groupid, $onlyactiveenrol);
 959      }
 960  
 961  }
 962