Search moodle.org's
Developer Documentation

See Release Notes

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

Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402] [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              ' grade_helper::get_lang_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 = grade_helper::get_lang_string($matrix[$direction], 'moodle');
 600          $arrow = $OUTPUT->pix_icon($pix[$direction], '', '', ['class' => 'sorticon']);
 601          return html_writer::link($sortlink, $arrow, ['title' => $strsort, 'aria-label' => $strsort, 'data-collapse' => 'sort']);
 602      }
 603  
 604      /**
 605       * Optionally blank out course/category totals if they contain any hidden items
 606       * @param string $courseid the course id
 607       * @param string $course_item an instance of grade_item
 608       * @param string $finalgrade the grade for the course_item
 609       * @return array[] containing values for 'grade', 'grademax', 'grademin', 'aggregationstatus' and 'aggregationweight'
 610       */
 611      protected function blank_hidden_total_and_adjust_bounds($courseid, $course_item, $finalgrade) {
 612          global $CFG, $DB;
 613          static $hiding_affected = null;//array of items in this course affected by hiding
 614  
 615          // If we're dealing with multiple users we need to know when we've moved on to a new user.
 616          static $previous_userid = null;
 617  
 618          // If we're dealing with multiple courses we need to know when we've moved on to a new course.
 619          static $previous_courseid = null;
 620  
 621          $coursegradegrade = grade_grade::fetch(array('userid'=>$this->user->id, 'itemid'=>$course_item->id));
 622          $grademin = $course_item->grademin;
 623          $grademax = $course_item->grademax;
 624          if ($coursegradegrade) {
 625              $grademin = $coursegradegrade->get_grade_min();
 626              $grademax = $coursegradegrade->get_grade_max();
 627          } else {
 628              $coursegradegrade = new grade_grade(array('userid'=>$this->user->id, 'itemid'=>$course_item->id), false);
 629          }
 630          $hint = $coursegradegrade->get_aggregation_hint();
 631          $aggregationstatus = $hint['status'];
 632          $aggregationweight = $hint['weight'];
 633  
 634          if (!is_array($this->showtotalsifcontainhidden)) {
 635              debugging('showtotalsifcontainhidden should be an array', DEBUG_DEVELOPER);
 636              $this->showtotalsifcontainhidden = array($courseid => $this->showtotalsifcontainhidden);
 637          }
 638  
 639          if ($this->showtotalsifcontainhidden[$courseid] == GRADE_REPORT_SHOW_REAL_TOTAL_IF_CONTAINS_HIDDEN) {
 640              return array('grade' => $finalgrade,
 641                           'grademin' => $grademin,
 642                           'grademax' => $grademax,
 643                           'aggregationstatus' => $aggregationstatus,
 644                           'aggregationweight' => $aggregationweight);
 645          }
 646  
 647          // If we've moved on to another course or user, reload the grades.
 648          if ($previous_userid != $this->user->id || $previous_courseid != $courseid) {
 649              $hiding_affected = null;
 650              $previous_userid = $this->user->id;
 651              $previous_courseid = $courseid;
 652          }
 653  
 654          if (!$hiding_affected) {
 655              $items = grade_item::fetch_all(array('courseid'=>$courseid));
 656              $grades = array();
 657              $sql = "SELECT g.*
 658                        FROM {grade_grades} g
 659                        JOIN {grade_items} gi ON gi.id = g.itemid
 660                       WHERE g.userid = {$this->user->id} AND gi.courseid = {$courseid}";
 661              if ($gradesrecords = $DB->get_records_sql($sql)) {
 662                  foreach ($gradesrecords as $grade) {
 663                      $grades[$grade->itemid] = new grade_grade($grade, false);
 664                  }
 665                  unset($gradesrecords);
 666              }
 667              foreach ($items as $itemid => $unused) {
 668                  if (!isset($grades[$itemid])) {
 669                      $grade_grade = new grade_grade();
 670                      $grade_grade->userid = $this->user->id;
 671                      $grade_grade->itemid = $items[$itemid]->id;
 672                      $grades[$itemid] = $grade_grade;
 673                  }
 674                  $grades[$itemid]->grade_item =& $items[$itemid];
 675              }
 676              $hiding_affected = grade_grade::get_hiding_affected($grades, $items);
 677          }
 678  
 679          //if the item definitely depends on a hidden item
 680          if (array_key_exists($course_item->id, $hiding_affected['altered']) ||
 681                  array_key_exists($course_item->id, $hiding_affected['alteredgrademin']) ||
 682                  array_key_exists($course_item->id, $hiding_affected['alteredgrademax']) ||
 683                  array_key_exists($course_item->id, $hiding_affected['alteredaggregationstatus']) ||
 684                  array_key_exists($course_item->id, $hiding_affected['alteredaggregationweight'])) {
 685              if (!$this->showtotalsifcontainhidden[$courseid] && array_key_exists($course_item->id, $hiding_affected['altered'])) {
 686                  // Hide the grade, but only when it has changed.
 687                  $finalgrade = null;
 688              } else {
 689                  //use reprocessed marks that exclude hidden items
 690                  if (array_key_exists($course_item->id, $hiding_affected['altered'])) {
 691                      $finalgrade = $hiding_affected['altered'][$course_item->id];
 692                  }
 693                  if (array_key_exists($course_item->id, $hiding_affected['alteredgrademin'])) {
 694                      $grademin = $hiding_affected['alteredgrademin'][$course_item->id];
 695                  }
 696                  if (array_key_exists($course_item->id, $hiding_affected['alteredgrademax'])) {
 697                      $grademax = $hiding_affected['alteredgrademax'][$course_item->id];
 698                  }
 699                  if (array_key_exists($course_item->id, $hiding_affected['alteredaggregationstatus'])) {
 700                      $aggregationstatus = $hiding_affected['alteredaggregationstatus'][$course_item->id];
 701                  }
 702                  if (array_key_exists($course_item->id, $hiding_affected['alteredaggregationweight'])) {
 703                      $aggregationweight = $hiding_affected['alteredaggregationweight'][$course_item->id];
 704                  }
 705  
 706                  if (!$this->showtotalsifcontainhidden[$courseid]) {
 707                      // If the course total is hidden we must hide the weight otherwise
 708                      // it can be used to compute the course total.
 709                      $aggregationstatus = 'unknown';
 710                      $aggregationweight = null;
 711                  }
 712              }
 713          } else if (array_key_exists($course_item->id, $hiding_affected['unknowngrades'])) {
 714              //not sure whether or not this item depends on a hidden item
 715              if (!$this->showtotalsifcontainhidden[$courseid]) {
 716                  //hide the grade
 717                  $finalgrade = null;
 718              } else {
 719                  //use reprocessed marks that exclude hidden items
 720                  $finalgrade = $hiding_affected['unknowngrades'][$course_item->id];
 721  
 722                  if (array_key_exists($course_item->id, $hiding_affected['alteredgrademin'])) {
 723                      $grademin = $hiding_affected['alteredgrademin'][$course_item->id];
 724                  }
 725                  if (array_key_exists($course_item->id, $hiding_affected['alteredgrademax'])) {
 726                      $grademax = $hiding_affected['alteredgrademax'][$course_item->id];
 727                  }
 728                  if (array_key_exists($course_item->id, $hiding_affected['alteredaggregationstatus'])) {
 729                      $aggregationstatus = $hiding_affected['alteredaggregationstatus'][$course_item->id];
 730                  }
 731                  if (array_key_exists($course_item->id, $hiding_affected['alteredaggregationweight'])) {
 732                      $aggregationweight = $hiding_affected['alteredaggregationweight'][$course_item->id];
 733                  }
 734              }
 735          }
 736  
 737          return array('grade' => $finalgrade, 'grademin' => $grademin, 'grademax' => $grademax, 'aggregationstatus'=>$aggregationstatus, 'aggregationweight'=>$aggregationweight);
 738      }
 739  
 740      /**
 741       * Optionally blank out course/category totals if they contain any hidden items
 742       * @deprecated since Moodle 2.8 - Call blank_hidden_total_and_adjust_bounds instead.
 743       * @param string $courseid the course id
 744       * @param string $course_item an instance of grade_item
 745       * @param string $finalgrade the grade for the course_item
 746       * @return string The new final grade
 747       */
 748      protected function blank_hidden_total($courseid, $course_item, $finalgrade) {
 749          // Note it is flawed to call this function directly because
 750          // the aggregated grade does not make sense without the updated min and max information.
 751  
 752          debugging('grade_report::blank_hidden_total() is deprecated.
 753                     Call grade_report::blank_hidden_total_and_adjust_bounds instead.', DEBUG_DEVELOPER);
 754          $result = $this->blank_hidden_total_and_adjust_bounds($courseid, $course_item, $finalgrade);
 755          return $result['grade'];
 756      }
 757  
 758      /**
 759       * Calculate average grade for a given grade item.
 760       * Based on calculate_averages function from grade/report/user/lib.php
 761       *
 762       * @param grade_item $gradeitem Grade item
 763       * @param array $info Ungraded grade items counts and report preferences.
 764       * @return array Average grade and meancount.
 765       */
 766      public static function calculate_average(grade_item $gradeitem, array $info): array {
 767  
 768          $meanselection = $info['report']['meanselection'];
 769          $totalcount = $info['report']['totalcount'];
 770          $ungradedcounts = $info['ungradedcounts'];
 771          $sumarray = $info['sumarray'];
 772  
 773          if (empty($sumarray[$gradeitem->id])) {
 774              $sumarray[$gradeitem->id] = 0;
 775          }
 776  
 777          if (empty($ungradedcounts[$gradeitem->id])) {
 778              $ungradedcounts = 0;
 779          } else {
 780              $ungradedcounts = $ungradedcounts[$gradeitem->id]->count;
 781          }
 782  
 783          // If they want the averages to include all grade items.
 784          if ($meanselection == GRADE_REPORT_MEAN_GRADED) {
 785              $meancount = $totalcount - $ungradedcounts;
 786          } else {
 787              // Bump up the sum by the number of ungraded items * grademin.
 788              $sumarray[$gradeitem->id] += ($ungradedcounts * $gradeitem->grademin);
 789              $meancount = $totalcount;
 790          }
 791  
 792          $aggr['meancount'] = $meancount;
 793  
 794          if (empty($sumarray[$gradeitem->id]) || $meancount == 0) {
 795              $aggr['average'] = null;
 796          } else {
 797              $sum = $sumarray[$gradeitem->id];
 798              $aggr['average'] = $sum / $meancount;
 799          }
 800          return $aggr;
 801      }
 802  
 803      /**
 804       * Get ungraded grade items info and sum of all grade items in a course.
 805       * Based on calculate_averages function from grade/report/user/lib.php
 806       *
 807       * @return array Ungraded grade items counts with report preferences.
 808       */
 809      public function ungraded_counts(): array {
 810          global $DB;
 811  
 812          $groupid = null;
 813          if (isset($this->gpr->groupid)) {
 814              $groupid = $this->gpr->groupid;
 815          }
 816  
 817          $info = [];
 818          $info['report'] = [
 819              'averagesdisplaytype' => $this->get_pref('averagesdisplaytype'),
 820              'averagesdecimalpoints' => $this->get_pref('averagesdecimalpoints'),
 821              'meanselection' => $this->get_pref('meanselection'),
 822              'shownumberofgrades' => $this->get_pref('shownumberofgrades'),
 823              'totalcount' => $this->get_numusers(!is_null($groupid)),
 824          ];
 825  
 826          // We want to query both the current context and parent contexts.
 827          list($relatedctxsql, $relatedctxparams) =
 828              $DB->get_in_or_equal($this->context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx');
 829  
 830          // Limit to users with a gradeable role ie students.
 831          list($gradebookrolessql, $gradebookrolesparams) =
 832              $DB->get_in_or_equal(explode(',', $this->gradebookroles), SQL_PARAMS_NAMED, 'grbr0');
 833  
 834          // Limit to users with an active enrolment.
 835          $defaultgradeshowactiveenrol = !empty($CFG->grade_report_showonlyactiveenrol);
 836          $showonlyactiveenrol = get_user_preferences('grade_report_showonlyactiveenrol', $defaultgradeshowactiveenrol);
 837          $showonlyactiveenrol = $showonlyactiveenrol ||
 838              !has_capability('moodle/course:viewsuspendedusers', $this->context);
 839          list($enrolledsql, $enrolledparams) = get_enrolled_sql($this->context, '', 0, $showonlyactiveenrol);
 840  
 841          $params = array_merge($this->groupwheresql_params, $gradebookrolesparams, $enrolledparams, $relatedctxparams);
 842          $params['courseid'] = $this->courseid;
 843  
 844          // Aggregate on whole course only.
 845          if (empty($groupid)) {
 846              $this->groupsql = null;
 847              $this->groupwheresql = null;
 848          }
 849  
 850          // Empty grades must be evaluated as grademin, NOT always 0.
 851          // This query returns a count of ungraded grades (NULL finalgrade OR no matching record in grade_grades table).
 852          // No join condition when joining grade_items and user to get a grade item row for every user.
 853          // Then left join with grade_grades and look for rows with null final grade
 854          // (which includes grade items with no grade_grade).
 855          $sql = "SELECT gi.id, COUNT(u.id) AS count
 856                        FROM {grade_items} gi
 857                        JOIN {user} u ON u.deleted = 0
 858                        JOIN ($enrolledsql) je ON je.id = u.id
 859                        JOIN (
 860                                 SELECT DISTINCT ra.userid
 861                                   FROM {role_assignments} ra
 862                                  WHERE ra.roleid $gradebookrolessql
 863                                    AND ra.contextid $relatedctxsql
 864                             ) rainner ON rainner.userid = u.id
 865                        LEFT JOIN {grade_grades} gg
 866                               ON (gg.itemid = gi.id AND gg.userid = u.id AND gg.finalgrade IS NOT NULL AND gg.hidden = 0)
 867                        $this->groupsql
 868                       WHERE gi.courseid = :courseid
 869                             AND gg.finalgrade IS NULL
 870                             $this->groupwheresql
 871                    GROUP BY gi.id";
 872          $info['ungradedcounts'] = $DB->get_records_sql($sql, $params);
 873  
 874          // Find sums of all grade items in course.
 875          $sql = "SELECT gg.itemid, SUM(gg.finalgrade) AS sum
 876                        FROM {grade_items} gi
 877                        JOIN {grade_grades} gg ON gg.itemid = gi.id
 878                        JOIN {user} u ON u.id = gg.userid
 879                        JOIN ($enrolledsql) je ON je.id = gg.userid
 880                        JOIN (
 881                                     SELECT DISTINCT ra.userid
 882                                       FROM {role_assignments} ra
 883                                      WHERE ra.roleid $gradebookrolessql
 884                                        AND ra.contextid $relatedctxsql
 885                             ) rainner ON rainner.userid = u.id
 886                        $this->groupsql
 887                       WHERE gi.courseid = :courseid
 888                         AND u.deleted = 0
 889                         AND gg.finalgrade IS NOT NULL
 890                         AND gg.hidden = 0
 891                         $this->groupwheresql
 892                    GROUP BY gg.itemid";
 893  
 894          $sumarray = [];
 895          $sums = $DB->get_recordset_sql($sql, $params);
 896          foreach ($sums as $itemid => $csum) {
 897              $sumarray[$itemid] = grade_floatval($csum->sum);
 898          }
 899          $sums->close();
 900          $info['sumarray'] = $sumarray;
 901  
 902          return $info;
 903      }
 904  
 905      /**
 906       * Get grade item type names in a course to use in filter dropdown.
 907       *
 908       * @return array Item types.
 909       */
 910      public function item_types(): array {
 911          global $DB, $CFG;
 912  
 913          $modnames = [];
 914          $sql = "(SELECT gi.itemmodule
 915                     FROM {grade_items} gi
 916                    WHERE gi.courseid = :courseid1
 917                      AND gi.itemmodule IS NOT NULL)
 918                   UNION
 919                  (SELECT gi1.itemtype
 920                     FROM {grade_items} gi1
 921                    WHERE gi1.courseid = :courseid2
 922                      AND gi1.itemtype = 'manual')";
 923  
 924          $itemtypes = $DB->get_records_sql($sql, ['courseid1' => $this->courseid, 'courseid2' => $this->courseid]);
 925          foreach ($itemtypes as $itemtype => $value) {
 926              if (file_exists("$CFG->dirroot/mod/$itemtype/lib.php")) {
 927                  $modnames[$itemtype] = get_string("modulename", $itemtype, null, true);
 928              } else if ($itemtype == 'manual') {
 929                  $modnames[$itemtype] = get_string('manualitem', 'grades', null, true);
 930              }
 931          }
 932  
 933          return $modnames;
 934      }
 935  
 936      /**
 937       * Load a valid list of gradable users in a course.
 938       *
 939       * @param int $courseid The course ID.
 940       * @param int|null $groupid The group ID (optional).
 941       * @return array A list of enrolled gradable users.
 942       */
 943      public static function get_gradable_users(int $courseid, ?int $groupid = null): array {
 944          global $CFG;
 945          require_once($CFG->dirroot . '/grade/lib.php');
 946  
 947          $context = context_course::instance($courseid);
 948          $defaultgradeshowactiveenrol = !empty($CFG->grade_report_showonlyactiveenrol);
 949          $onlyactiveenrol = get_user_preferences('grade_report_showonlyactiveenrol', $defaultgradeshowactiveenrol) ||
 950              !has_capability('moodle/course:viewsuspendedusers', $context);
 951  
 952          return get_gradable_users($courseid, $groupid, $onlyactiveenrol);
 953      }
 954  
 955  }
 956