Search moodle.org's
Developer Documentation

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
  • Differences Between: [Versions 310 and 311] [Versions 311 and 400] [Versions 37 and 311] [Versions 38 and 311] [Versions 39 and 311]

       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   * Definition of the grader report class
      19   *
      20   * @package   gradereport_grader
      21   * @copyright 2007 Nicolas Connault
      22   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
      23   */
      24  
      25  require_once($CFG->dirroot . '/grade/report/lib.php');
      26  require_once($CFG->libdir.'/tablelib.php');
      27  
      28  /**
      29   * Class providing an API for the grader report building and displaying.
      30   * @uses grade_report
      31   * @copyright 2007 Nicolas Connault
      32   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
      33   */
      34  class grade_report_grader extends grade_report {
      35      /**
      36       * The final grades.
      37       * @var array $grades
      38       */
      39      public $grades;
      40  
      41      /**
      42       * Contains all the grades for the course - even the ones not displayed in the grade tree.
      43       *
      44       * @var array $allgrades
      45       */
      46      private $allgrades;
      47  
      48      /**
      49       * Contains all grade items expect GRADE_TYPE_NONE.
      50       *
      51       * @var array $allgradeitems
      52       */
      53      private $allgradeitems;
      54  
      55      /**
      56       * Array of errors for bulk grades updating.
      57       * @var array $gradeserror
      58       */
      59      public $gradeserror = array();
      60  
      61      // SQL-RELATED
      62  
      63      /**
      64       * The id of the grade_item by which this report will be sorted.
      65       * @var int $sortitemid
      66       */
      67      public $sortitemid;
      68  
      69      /**
      70       * Sortorder used in the SQL selections.
      71       * @var int $sortorder
      72       */
      73      public $sortorder;
      74  
      75      /**
      76       * An SQL fragment affecting the search for users.
      77       * @var string $userselect
      78       */
      79      public $userselect;
      80  
      81      /**
      82       * The bound params for $userselect
      83       * @var array $userselectparams
      84       */
      85      public $userselectparams = array();
      86  
      87      /**
      88       * List of collapsed categories from user preference
      89       * @var array $collapsed
      90       */
      91      public $collapsed;
      92  
      93      /**
      94       * A count of the rows, used for css classes.
      95       * @var int $rowcount
      96       */
      97      public $rowcount = 0;
      98  
      99      /**
     100       * Capability check caching
     101       * @var boolean $canviewhidden
     102       */
     103      public $canviewhidden;
     104  
     105      /**
     106       * Length at which feedback will be truncated (to the nearest word) and an ellipsis be added.
     107       * TODO replace this by a report preference
     108       * @var int $feedback_trunc_length
     109       */
     110      protected $feedback_trunc_length = 50;
     111  
     112      /**
     113       * Allow category grade overriding
     114       * @var bool $overridecat
     115       */
     116      protected $overridecat;
     117  
     118      /**
     119       * Constructor. Sets local copies of user preferences and initialises grade_tree.
     120       * @param int $courseid
     121       * @param object $gpr grade plugin return tracking object
     122       * @param string $context
     123       * @param int $page The current page being viewed (when report is paged)
     124       * @param int $sortitemid The id of the grade_item by which to sort the table
     125       */
     126      public function __construct($courseid, $gpr, $context, $page=null, $sortitemid=null) {
     127          global $CFG;
     128          parent::__construct($courseid, $gpr, $context, $page);
     129  
     130          $this->canviewhidden = has_capability('moodle/grade:viewhidden', context_course::instance($this->course->id));
     131  
     132          // load collapsed settings for this report
     133          $this->collapsed = static::get_collapsed_preferences($this->course->id);
     134  
     135          if (empty($CFG->enableoutcomes)) {
     136              $nooutcomes = false;
     137          } else {
     138              $nooutcomes = get_user_preferences('grade_report_shownooutcomes');
     139          }
     140  
     141          // if user report preference set or site report setting set use it, otherwise use course or site setting
     142          $switch = $this->get_pref('aggregationposition');
     143          if ($switch == '') {
     144              $switch = grade_get_setting($this->courseid, 'aggregationposition', $CFG->grade_aggregationposition);
     145          }
     146  
     147          // Grab the grade_tree for this course
     148          $this->gtree = new grade_tree($this->courseid, true, $switch, $this->collapsed, $nooutcomes);
     149  
     150          $this->sortitemid = $sortitemid;
     151  
     152          // base url for sorting by first/last name
     153  
     154          $this->baseurl = new moodle_url('index.php', array('id' => $this->courseid));
     155  
     156          $studentsperpage = $this->get_students_per_page();
     157          if (!empty($this->page) && !empty($studentsperpage)) {
     158              $this->baseurl->params(array('perpage' => $studentsperpage, 'page' => $this->page));
     159          }
     160  
     161          $this->pbarurl = new moodle_url('/grade/report/grader/index.php', array('id' => $this->courseid));
     162  
     163          $this->setup_groups();
     164          $this->setup_users();
     165          $this->setup_sortitemid();
     166  
     167          $this->overridecat = (bool)get_config('moodle', 'grade_overridecat');
     168      }
     169  
     170      /**
     171       * Processes the data sent by the form (grades and feedbacks).
     172       * Caller is responsible for all access control checks
     173       * @param array $data form submission (with magic quotes)
     174       * @return array empty array if success, array of warnings if something fails.
     175       */
     176      public function process_data($data) {
     177          global $DB;
     178          $warnings = array();
     179  
     180          $separategroups = false;
     181          $mygroups       = array();
     182          if ($this->groupmode == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $this->context)) {
     183              $separategroups = true;
     184              $mygroups = groups_get_user_groups($this->course->id);
     185              $mygroups = $mygroups[0]; // ignore groupings
     186              // reorder the groups fro better perf below
     187              $current = array_search($this->currentgroup, $mygroups);
     188              if ($current !== false) {
     189                  unset($mygroups[$current]);
     190                  array_unshift($mygroups, $this->currentgroup);
     191              }
     192          }
     193          $viewfullnames = has_capability('moodle/site:viewfullnames', $this->context);
     194  
     195          // always initialize all arrays
     196          $queue = array();
     197  
     198          $this->load_users();
     199          $this->load_final_grades();
     200  
     201          // Were any changes made?
     202          $changedgrades = false;
     203          $timepageload = clean_param($data->timepageload, PARAM_INT);
     204  
     205          foreach ($data as $varname => $students) {
     206  
     207              $needsupdate = false;
     208  
     209              // skip, not a grade nor feedback
     210              if (strpos($varname, 'grade') === 0) {
     211                  $datatype = 'grade';
     212              } else if (strpos($varname, 'feedback') === 0) {
     213                  $datatype = 'feedback';
     214              } else {
     215                  continue;
     216              }
     217  
     218              foreach ($students as $userid => $items) {
     219                  $userid = clean_param($userid, PARAM_INT);
     220                  foreach ($items as $itemid => $postedvalue) {
     221                      $itemid = clean_param($itemid, PARAM_INT);
     222  
     223                      // Was change requested?
     224                      $oldvalue = $this->grades[$userid][$itemid];
     225                      if ($datatype === 'grade') {
     226                          // If there was no grade and there still isn't
     227                          if (is_null($oldvalue->finalgrade) && $postedvalue == -1) {
     228                              // -1 means no grade
     229                              continue;
     230                          }
     231  
     232                          // If the grade item uses a custom scale
     233                          if (!empty($oldvalue->grade_item->scaleid)) {
     234  
     235                              if ((int)$oldvalue->finalgrade === (int)$postedvalue) {
     236                                  continue;
     237                              }
     238                          } else {
     239                              // The grade item uses a numeric scale
     240  
     241                              // Format the finalgrade from the DB so that it matches the grade from the client
     242                              if ($postedvalue === format_float($oldvalue->finalgrade, $oldvalue->grade_item->get_decimals())) {
     243                                  continue;
     244                              }
     245                          }
     246  
     247                          $changedgrades = true;
     248  
     249                      } else if ($datatype === 'feedback') {
     250                          // If quick grading is on, feedback needs to be compared without line breaks.
     251                          if ($this->get_pref('quickgrading')) {
     252                              $oldvalue->feedback = preg_replace("/\r\n|\r|\n/", "", $oldvalue->feedback);
     253                          }
     254                          if (($oldvalue->feedback === $postedvalue) or ($oldvalue->feedback === null and empty($postedvalue))) {
     255                              continue;
     256                          }
     257                      }
     258  
     259                      if (!$gradeitem = grade_item::fetch(array('id'=>$itemid, 'courseid'=>$this->courseid))) {
     260                          print_error('invalidgradeitemid');
     261                      }
     262  
     263                      // Pre-process grade
     264                      if ($datatype == 'grade') {
     265                          $feedback = false;
     266                          $feedbackformat = false;
     267                          if ($gradeitem->gradetype == GRADE_TYPE_SCALE) {
     268                              if ($postedvalue == -1) { // -1 means no grade
     269                                  $finalgrade = null;
     270                              } else {
     271                                  $finalgrade = $postedvalue;
     272                              }
     273                          } else {
     274                              $finalgrade = unformat_float($postedvalue);
     275                          }
     276  
     277                          $errorstr = '';
     278                          $skip = false;
     279  
     280                          $dategraded = $oldvalue->get_dategraded();
     281                          if (!empty($dategraded) && $timepageload < $dategraded) {
     282                              // Warn if the grade was updated while we were editing this form.
     283                              $errorstr = 'gradewasmodifiedduringediting';
     284                              $skip = true;
     285                          } else if (!is_null($finalgrade)) {
     286                              // Warn if the grade is out of bounds.
     287                              $bounded = $gradeitem->bounded_grade($finalgrade);
     288                              if ($bounded > $finalgrade) {
     289                                  $errorstr = 'lessthanmin';
     290                              } else if ($bounded < $finalgrade) {
     291                                  $errorstr = 'morethanmax';
     292                              }
     293                          }
     294  
     295                          if ($errorstr) {
     296                              $userfieldsapi = \core_user\fields::for_name();
     297                              $userfields = 'id, ' . $userfieldsapi->get_sql('', false, '', '', false)->selects;
     298                              $user = $DB->get_record('user', array('id' => $userid), $userfields);
     299                              $gradestr = new stdClass();
     300                              $gradestr->username = fullname($user, $viewfullnames);
     301                              $gradestr->itemname = $gradeitem->get_name();
     302                              $warnings[] = get_string($errorstr, 'grades', $gradestr);
     303                              if ($skip) {
     304                                  // Skipping the update of this grade it failed the tests above.
     305                                  continue;
     306                              }
     307                          }
     308  
     309                      } else if ($datatype == 'feedback') {
     310                          $finalgrade = false;
     311                          $trimmed = trim($postedvalue);
     312                          if (empty($trimmed)) {
     313                               $feedback = null;
     314                          } else {
     315                               $feedback = $postedvalue;
     316                          }
     317                      }
     318  
     319                      // group access control
     320                      if ($separategroups) {
     321                          // note: we can not use $this->currentgroup because it would fail badly
     322                          //       when having two browser windows each with different group
     323                          $sharinggroup = false;
     324                          foreach ($mygroups as $groupid) {
     325                              if (groups_is_member($groupid, $userid)) {
     326                                  $sharinggroup = true;
     327                                  break;
     328                              }
     329                          }
     330                          if (!$sharinggroup) {
     331                              // either group membership changed or somebody is hacking grades of other group
     332                              $warnings[] = get_string('errorsavegrade', 'grades');
     333                              continue;
     334                          }
     335                      }
     336  
     337                      $gradeitem->update_final_grade($userid, $finalgrade, 'gradebook', $feedback, FORMAT_MOODLE);
     338  
     339                      // We can update feedback without reloading the grade item as it doesn't affect grade calculations
     340                      if ($datatype === 'feedback') {
     341                          $this->grades[$userid][$itemid]->feedback = $feedback;
     342                      }
     343                  }
     344              }
     345          }
     346  
     347          if ($changedgrades) {
     348              // If a final grade was overriden reload grades so dependent grades like course total will be correct
     349              $this->grades = null;
     350          }
     351  
     352          return $warnings;
     353      }
     354  
     355  
     356      /**
     357       * Setting the sort order, this depends on last state
     358       * all this should be in the new table class that we might need to use
     359       * for displaying grades.
     360       */
     361      private function setup_sortitemid() {
     362  
     363          global $SESSION;
     364  
     365          if (!isset($SESSION->gradeuserreport)) {
     366              $SESSION->gradeuserreport = new stdClass();
     367          }
     368  
     369          if ($this->sortitemid) {
     370              if (!isset($SESSION->gradeuserreport->sort)) {
     371                  if ($this->sortitemid == 'firstname' || $this->sortitemid == 'lastname') {
     372                      $this->sortorder = $SESSION->gradeuserreport->sort = 'ASC';
     373                  } else {
     374                      $this->sortorder = $SESSION->gradeuserreport->sort = 'DESC';
     375                  }
     376              } else {
     377                  // this is the first sort, i.e. by last name
     378                  if (!isset($SESSION->gradeuserreport->sortitemid)) {
     379                      if ($this->sortitemid == 'firstname' || $this->sortitemid == 'lastname') {
     380                          $this->sortorder = $SESSION->gradeuserreport->sort = 'ASC';
     381                      } else {
     382                          $this->sortorder = $SESSION->gradeuserreport->sort = 'DESC';
     383                      }
     384                  } else if ($SESSION->gradeuserreport->sortitemid == $this->sortitemid) {
     385                      // same as last sort
     386                      if ($SESSION->gradeuserreport->sort == 'ASC') {
     387                          $this->sortorder = $SESSION->gradeuserreport->sort = 'DESC';
     388                      } else {
     389                          $this->sortorder = $SESSION->gradeuserreport->sort = 'ASC';
     390                      }
     391                  } else {
     392                      if ($this->sortitemid == 'firstname' || $this->sortitemid == 'lastname') {
     393                          $this->sortorder = $SESSION->gradeuserreport->sort = 'ASC';
     394                      } else {
     395                          $this->sortorder = $SESSION->gradeuserreport->sort = 'DESC';
     396                      }
     397                  }
     398              }
     399              $SESSION->gradeuserreport->sortitemid = $this->sortitemid;
     400          } else {
     401              // not requesting sort, use last setting (for paging)
     402  
     403              if (isset($SESSION->gradeuserreport->sortitemid)) {
     404                  $this->sortitemid = $SESSION->gradeuserreport->sortitemid;
     405              } else {
     406                  $this->sortitemid = 'lastname';
     407              }
     408  
     409              if (isset($SESSION->gradeuserreport->sort)) {
     410                  $this->sortorder = $SESSION->gradeuserreport->sort;
     411              } else {
     412                  $this->sortorder = 'ASC';
     413              }
     414          }
     415      }
     416  
     417      /**
     418       * pulls out the userids of the users to be display, and sorts them
     419       */
     420      public function load_users() {
     421          global $CFG, $DB;
     422  
     423          if (!empty($this->users)) {
     424              return;
     425          }
     426          $this->setup_users();
     427  
     428          // Limit to users with a gradeable role.
     429          list($gradebookrolessql, $gradebookrolesparams) = $DB->get_in_or_equal(explode(',', $this->gradebookroles), SQL_PARAMS_NAMED, 'grbr0');
     430  
     431          // Check the status of showing only active enrolments.
     432          $coursecontext = $this->context->get_course_context(true);
     433          $defaultgradeshowactiveenrol = !empty($CFG->grade_report_showonlyactiveenrol);
     434          $showonlyactiveenrol = get_user_preferences('grade_report_showonlyactiveenrol', $defaultgradeshowactiveenrol);
     435          $showonlyactiveenrol = $showonlyactiveenrol || !has_capability('moodle/course:viewsuspendedusers', $coursecontext);
     436  
     437          // Limit to users with an active enrollment.
     438          list($enrolledsql, $enrolledparams) = get_enrolled_sql($this->context, '', 0, $showonlyactiveenrol);
     439  
     440          // Fields we need from the user table.
     441          // TODO Does not support custom user profile fields (MDL-70456).
     442          $userfieldsapi = \core_user\fields::for_identity($this->context, false)->with_userpic();
     443          $userfields = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
     444  
     445          // We want to query both the current context and parent contexts.
     446          list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($this->context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx');
     447  
     448          // If the user has clicked one of the sort asc/desc arrows.
     449          if (is_numeric($this->sortitemid)) {
     450              $params = array_merge(array('gitemid' => $this->sortitemid), $gradebookrolesparams, $this->userwheresql_params,
     451                      $this->groupwheresql_params, $enrolledparams, $relatedctxparams);
     452  
     453              $sortjoin = "LEFT JOIN {grade_grades} g ON g.userid = u.id AND g.itemid = $this->sortitemid";
     454              $sort = "g.finalgrade $this->sortorder, u.idnumber, u.lastname, u.firstname, u.email";
     455          } else {
     456              $sortjoin = '';
     457              switch($this->sortitemid) {
     458                  case 'lastname':
     459                      $sort = "u.lastname $this->sortorder, u.firstname $this->sortorder, u.idnumber, u.email";
     460                      break;
     461                  case 'firstname':
     462                      $sort = "u.firstname $this->sortorder, u.lastname $this->sortorder, u.idnumber, u.email";
     463                      break;
     464                  case 'email':
     465                      $sort = "u.email $this->sortorder, u.firstname, u.lastname, u.idnumber";
     466                      break;
     467                  case 'idnumber':
     468                  default:
     469                      $sort = "u.idnumber $this->sortorder, u.firstname, u.lastname, u.email";
     470                      break;
     471              }
     472  
     473              $params = array_merge($gradebookrolesparams, $this->userwheresql_params, $this->groupwheresql_params, $enrolledparams, $relatedctxparams);
     474          }
     475  
     476          $sql = "SELECT $userfields
     477                    FROM {user} u
     478                    JOIN ($enrolledsql) je ON je.id = u.id
     479                         $this->groupsql
     480                         $sortjoin
     481                    JOIN (
     482                             SELECT DISTINCT ra.userid
     483                               FROM {role_assignments} ra
     484                              WHERE ra.roleid IN ($this->gradebookroles)
     485                                AND ra.contextid $relatedctxsql
     486                         ) rainner ON rainner.userid = u.id
     487                     AND u.deleted = 0
     488                     $this->userwheresql
     489                     $this->groupwheresql
     490                ORDER BY $sort";
     491          $studentsperpage = $this->get_students_per_page();
     492          $this->users = $DB->get_records_sql($sql, $params, $studentsperpage * $this->page, $studentsperpage);
     493  
     494          if (empty($this->users)) {
     495              $this->userselect = '';
     496              $this->users = array();
     497              $this->userselect_params = array();
     498          } else {
     499              list($usql, $uparams) = $DB->get_in_or_equal(array_keys($this->users), SQL_PARAMS_NAMED, 'usid0');
     500              $this->userselect = "AND g.userid $usql";
     501              $this->userselect_params = $uparams;
     502  
     503              // First flag everyone as not suspended.
     504              foreach ($this->users as $user) {
     505                  $this->users[$user->id]->suspendedenrolment = false;
     506              }
     507  
     508              // If we want to mix both suspended and not suspended users, let's find out who is suspended.
     509              if (!$showonlyactiveenrol) {
     510                  $sql = "SELECT ue.userid
     511                            FROM {user_enrolments} ue
     512                            JOIN {enrol} e ON e.id = ue.enrolid
     513                           WHERE ue.userid $usql
     514                                 AND ue.status = :uestatus
     515                                 AND e.status = :estatus
     516                                 AND e.courseid = :courseid
     517                                 AND ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2)
     518                        GROUP BY ue.userid";
     519  
     520                  $time = time();
     521                  $params = array_merge($uparams, array('estatus' => ENROL_INSTANCE_ENABLED, 'uestatus' => ENROL_USER_ACTIVE,
     522                          'courseid' => $coursecontext->instanceid, 'now1' => $time, 'now2' => $time));
     523                  $useractiveenrolments = $DB->get_records_sql($sql, $params);
     524  
     525                  foreach ($this->users as $user) {
     526                      $this->users[$user->id]->suspendedenrolment = !array_key_exists($user->id, $useractiveenrolments);
     527                  }
     528              }
     529          }
     530          return $this->users;
     531      }
     532  
     533      /**
     534       * Load all grade items.
     535       */
     536      protected function get_allgradeitems() {
     537          if (!empty($this->allgradeitems)) {
     538              return $this->allgradeitems;
     539          }
     540          $allgradeitems = grade_item::fetch_all(array('courseid' => $this->courseid));
     541          // But hang on - don't include ones which are set to not show the grade at all.
     542          $this->allgradeitems = array_filter($allgradeitems, function($item) {
     543              return $item->gradetype != GRADE_TYPE_NONE;
     544          });
     545  
     546          return $this->allgradeitems;
     547      }
     548  
     549      /**
     550       * we supply the userids in this query, and get all the grades
     551       * pulls out all the grades, this does not need to worry about paging
     552       */
     553      public function load_final_grades() {
     554          global $CFG, $DB;
     555  
     556          if (!empty($this->grades)) {
     557              return;
     558          }
     559  
     560          if (empty($this->users)) {
     561              return;
     562          }
     563  
     564          // please note that we must fetch all grade_grades fields if we want to construct grade_grade object from it!
     565          $params = array_merge(array('courseid'=>$this->courseid), $this->userselect_params);
     566          $sql = "SELECT g.*
     567                    FROM {grade_items} gi,
     568                         {grade_grades} g
     569                   WHERE g.itemid = gi.id AND gi.courseid = :courseid {$this->userselect}";
     570  
     571          $userids = array_keys($this->users);
     572          $allgradeitems = $this->get_allgradeitems();
     573  
     574          if ($grades = $DB->get_records_sql($sql, $params)) {
     575              foreach ($grades as $graderec) {
     576                  $grade = new grade_grade($graderec, false);
     577                  if (!empty($allgradeitems[$graderec->itemid])) {
     578                      // Note: Filter out grades which have a grade type of GRADE_TYPE_NONE.
     579                      // Only grades without this type are present in $allgradeitems.
     580                      $this->allgrades[$graderec->userid][$graderec->itemid] = $grade;
     581                  }
     582                  if (in_array($graderec->userid, $userids) and array_key_exists($graderec->itemid, $this->gtree->get_items())) { // some items may not be present!!
     583                      $this->grades[$graderec->userid][$graderec->itemid] = $grade;
     584                      $this->grades[$graderec->userid][$graderec->itemid]->grade_item = $this->gtree->get_item($graderec->itemid); // db caching
     585                  }
     586              }
     587          }
     588  
     589          // prefil grades that do not exist yet
     590          foreach ($userids as $userid) {
     591              foreach ($this->gtree->get_items() as $itemid => $unused) {
     592                  if (!isset($this->grades[$userid][$itemid])) {
     593                      $this->grades[$userid][$itemid] = new grade_grade();
     594                      $this->grades[$userid][$itemid]->itemid = $itemid;
     595                      $this->grades[$userid][$itemid]->userid = $userid;
     596                      $this->grades[$userid][$itemid]->grade_item = $this->gtree->get_item($itemid); // db caching
     597  
     598                      $this->allgrades[$userid][$itemid] = $this->grades[$userid][$itemid];
     599                  }
     600              }
     601          }
     602  
     603          // Pre fill grades for any remaining items which might be collapsed.
     604          foreach ($userids as $userid) {
     605              foreach ($allgradeitems as $itemid => $gradeitem) {
     606                  if (!isset($this->allgrades[$userid][$itemid])) {
     607                      $this->allgrades[$userid][$itemid] = new grade_grade();
     608                      $this->allgrades[$userid][$itemid]->itemid = $itemid;
     609                      $this->allgrades[$userid][$itemid]->userid = $userid;
     610                      $this->allgrades[$userid][$itemid]->grade_item = $gradeitem;
     611                  }
     612              }
     613          }
     614      }
     615  
     616      /**
     617       * Gets html toggle
     618       * @deprecated since Moodle 2.4 as it appears not to be used any more.
     619       */
     620      public function get_toggles_html() {
     621          throw new coding_exception('get_toggles_html() can not be used any more');
     622      }
     623  
     624      /**
     625       * Prints html toggle
     626       * @deprecated since 2.4 as it appears not to be used any more.
     627       * @param unknown $type
     628       */
     629      public function print_toggle($type) {
     630          throw new coding_exception('print_toggle() can not be used any more');
     631      }
     632  
     633      /**
     634       * Builds and returns the rows that will make up the left part of the grader report
     635       * This consists of student names and icons, links to user reports and id numbers, as well
     636       * as header cells for these columns. It also includes the fillers required for the
     637       * categories displayed on the right side of the report.
     638       * @param boolean $displayaverages whether to display average rows in the table
     639       * @return array Array of html_table_row objects
     640       */
     641      public function get_left_rows($displayaverages) {
     642          global $CFG, $USER, $OUTPUT;
     643  
     644          $rows = array();
     645  
     646          $showuserimage = $this->get_pref('showuserimage');
     647          // FIXME: MDL-52678 This get_capability_info is hacky and we should have an API for inserting grade row links instead.
     648          $canseeuserreport = false;
     649          $canseesingleview = false;
     650          if (get_capability_info('gradereport/'.$CFG->grade_profilereport.':view')) {
     651              $canseeuserreport = has_capability('gradereport/'.$CFG->grade_profilereport.':view', $this->context);
     652          }
     653          if (get_capability_info('gradereport/singleview:view')) {
     654              $canseesingleview = has_all_capabilities(array('gradereport/singleview:view', 'moodle/grade:viewall',
     655              'moodle/grade:edit'), $this->context);
     656          }
     657          $hasuserreportcell = $canseeuserreport || $canseesingleview;
     658          $viewfullnames = has_capability('moodle/site:viewfullnames', $this->context);
     659  
     660          $strfeedback  = $this->get_lang_string("feedback");
     661  
     662          // TODO Does not support custom user profile fields (MDL-70456).
     663          $extrafields = \core_user\fields::get_identity_fields($this->context, false);
     664  
     665          $arrows = $this->get_sort_arrows($extrafields);
     666  
     667          $colspan = 1 + $hasuserreportcell + count($extrafields);
     668  
     669          $levels = count($this->gtree->levels) - 1;
     670  
     671          $fillercell = new html_table_cell();
     672          $fillercell->header = true;
     673          $fillercell->attributes['scope'] = 'col';
     674          $fillercell->attributes['class'] = 'cell topleft';
     675          $fillercell->text = html_writer::span(get_string('participants'), 'accesshide');
     676          $fillercell->colspan = $colspan;
     677          $fillercell->rowspan = $levels;
     678          $row = new html_table_row(array($fillercell));
     679          $rows[] = $row;
     680  
     681          for ($i = 1; $i < $levels; $i++) {
     682              $row = new html_table_row();
     683              $rows[] = $row;
     684          }
     685  
     686          $headerrow = new html_table_row();
     687          $headerrow->attributes['class'] = 'heading';
     688  
     689          $studentheader = new html_table_cell();
     690          // The browser's scrollbar may partly cover (in certain operative systems) the content in the student header
     691          // when horizontally scrolling through the table contents (most noticeable when in RTL mode).
     692          // Therefore, add slight padding on the left or right when using RTL mode.
     693          $studentheader->attributes['class'] = "header pl-3";
     694          $studentheader->scope = 'col';
     695          $studentheader->header = true;
     696          $studentheader->id = 'studentheader';
     697          if ($hasuserreportcell) {
     698              $studentheader->colspan = 2;
     699          }
     700          $studentheader->text = $arrows['studentname'];
     701  
     702          $headerrow->cells[] = $studentheader;
     703  
     704          foreach ($extrafields as $field) {
     705              $fieldheader = new html_table_cell();
     706              $fieldheader->attributes['class'] = 'header userfield user' . $field;
     707              $fieldheader->scope = 'col';
     708              $fieldheader->header = true;
     709              $fieldheader->text = $arrows[$field];
     710  
     711              $headerrow->cells[] = $fieldheader;
     712          }
     713  
     714          $rows[] = $headerrow;
     715  
     716          $rows = $this->get_left_icons_row($rows, $colspan);
     717  
     718          $suspendedstring = null;
     719  
     720          $usercount = 0;
     721          foreach ($this->users as $userid => $user) {
     722              $userrow = new html_table_row();
     723              $userrow->id = 'fixed_user_'.$userid;
     724              $userrow->attributes['class'] = ($usercount % 2) ? 'userrow even' : 'userrow odd';
     725  
     726              $usercell = new html_table_cell();
     727              $usercell->attributes['class'] = ($usercount % 2) ? 'header user even' : 'header user odd';
     728              $usercount++;
     729  
     730              $usercell->header = true;
     731              $usercell->scope = 'row';
     732  
     733              if ($showuserimage) {
     734                  $usercell->text = $OUTPUT->user_picture($user, ['link' => false, 'visibletoscreenreaders' => false]);
     735              }
     736  
     737              $fullname = fullname($user, $viewfullnames);
     738              $usercell->text = html_writer::link(
     739                      new moodle_url('/user/view.php', ['id' => $user->id, 'course' => $this->course->id]),
     740                      $usercell->text . $fullname,
     741                      ['class' => 'username']
     742              );
     743  
     744              if (!empty($user->suspendedenrolment)) {
     745                  $usercell->attributes['class'] .= ' usersuspended';
     746  
     747                  //may be lots of suspended users so only get the string once
     748                  if (empty($suspendedstring)) {
     749                      $suspendedstring = get_string('userenrolmentsuspended', 'grades');
     750                  }
     751                  $icon = $OUTPUT->pix_icon('i/enrolmentsuspended', $suspendedstring);
     752                  $usercell->text .= html_writer::tag('span', $icon, array('class'=>'usersuspendedicon'));
     753              }
     754              // The browser's scrollbar may partly cover (in certain operative systems) the content in the user cells
     755              // when horizontally scrolling through the table contents (most noticeable when in RTL mode).
     756              // Therefore, add slight padding on the left or right when using RTL mode.
     757              $usercell->attributes['class'] .= ' pl-3';
     758  
     759              $userrow->cells[] = $usercell;
     760  
     761              $userreportcell = new html_table_cell();
     762              $userreportcell->attributes['class'] = 'userreport';
     763              $userreportcell->header = false;
     764              if ($canseeuserreport) {
     765                  $a = new stdClass();
     766                  $a->user = $fullname;
     767                  $strgradesforuser = get_string('gradesforuser', 'grades', $a);
     768                  $url = new moodle_url('/grade/report/'.$CFG->grade_profilereport.'/index.php',
     769                          ['userid' => $user->id, 'id' => $this->course->id]);
     770                  $userreportcell->text .= $OUTPUT->action_icon($url, new pix_icon('t/grades', ''), null,
     771                          ['title' => $strgradesforuser, 'aria-label' => $strgradesforuser]);
     772              }
     773  
     774              if ($canseesingleview) {
     775                  $strsingleview = get_string('singleview', 'grades', $fullname);
     776                  $url = new moodle_url('/grade/report/singleview/index.php',
     777                          ['id' => $this->course->id, 'itemid' => $user->id, 'item' => 'user']);
     778                  $singleview = $OUTPUT->action_icon($url, new pix_icon('t/editstring', ''), null,
     779                          ['title' => $strsingleview, 'aria-label' => $strsingleview]);
     780                  $userreportcell->text .= $singleview;
     781              }
     782  
     783              if ($userreportcell->text) {
     784                  $userrow->cells[] = $userreportcell;
     785              }
     786  
     787              foreach ($extrafields as $field) {
     788                  $fieldcell = new html_table_cell();
     789                  $fieldcell->attributes['class'] = 'userfield user' . $field;
     790                  $fieldcell->header = false;
     791                  $fieldcell->text = s($user->{$field});
     792                  $userrow->cells[] = $fieldcell;
     793              }
     794  
     795              $userrow->attributes['data-uid'] = $userid;
     796              $rows[] = $userrow;
     797          }
     798  
     799          $rows = $this->get_left_range_row($rows, $colspan);
     800          if ($displayaverages) {
     801              $rows = $this->get_left_avg_row($rows, $colspan, true);
     802              $rows = $this->get_left_avg_row($rows, $colspan);
     803          }
     804  
     805          return $rows;
     806      }
     807  
     808      /**
     809       * Builds and returns the rows that will make up the right part of the grader report
     810       * @param boolean $displayaverages whether to display average rows in the table
     811       * @return array Array of html_table_row objects
     812       */
     813      public function get_right_rows($displayaverages) {
     814          global $CFG, $USER, $OUTPUT, $DB, $PAGE;
     815  
     816          $rows = array();
     817          $this->rowcount = 0;
     818          $numrows = count($this->gtree->get_levels());
     819          $numusers = count($this->users);
     820          $gradetabindex = 1;
     821          $columnstounset = array();
     822          $strgrade = $this->get_lang_string('gradenoun');
     823          $strfeedback  = $this->get_lang_string("feedback");
     824          $arrows = $this->get_sort_arrows();
     825  
     826          $jsarguments = array(
     827              'cfg'       => array('ajaxenabled'=>false),
     828              'items'     => array(),
     829              'users'     => array(),
     830              'feedback'  => array(),
     831              'grades'    => array()
     832          );
     833          $jsscales = array();
     834  
     835          // Get preferences once.
     836          $showactivityicons = $this->get_pref('showactivityicons');
     837          $quickgrading = $this->get_pref('quickgrading');
     838          $showquickfeedback = $this->get_pref('showquickfeedback');
     839          $enableajax = $this->get_pref('enableajax');
     840          $showanalysisicon = $this->get_pref('showanalysisicon');
     841  
     842          // Get strings which are re-used inside the loop.
     843          $strftimedatetimeshort = get_string('strftimedatetimeshort');
     844          $strexcludedgrades = get_string('excluded', 'grades');
     845          $strerror = get_string('error');
     846  
     847          $viewfullnames = has_capability('moodle/site:viewfullnames', $this->context);
     848  
     849          foreach ($this->gtree->get_levels() as $key => $row) {
     850              $headingrow = new html_table_row();
     851              $headingrow->attributes['class'] = 'heading_name_row';
     852  
     853              foreach ($row as $columnkey => $element) {
     854                  $sortlink = clone($this->baseurl);
     855                  if (isset($element['object']->id)) {
     856                      $sortlink->param('sortitemid', $element['object']->id);
     857                  }
     858  
     859                  $eid    = $element['eid'];
     860                  $object = $element['object'];
     861                  $type   = $element['type'];
     862                  $categorystate = @$element['categorystate'];
     863  
     864                  if (!empty($element['colspan'])) {
     865                      $colspan = $element['colspan'];
     866                  } else {
     867                      $colspan = 1;
     868                  }
     869  
     870                  if (!empty($element['depth'])) {
     871                      $catlevel = 'catlevel'.$element['depth'];
     872                  } else {
     873                      $catlevel = '';
     874                  }
     875  
     876                  // Element is a filler
     877                  if ($type == 'filler' or $type == 'fillerfirst' or $type == 'fillerlast') {
     878                      $fillercell = new html_table_cell();
     879                      $fillercell->attributes['class'] = $type . ' ' . $catlevel;
     880                      $fillercell->colspan = $colspan;
     881                      $fillercell->text = '&nbsp;';
     882  
     883                      // This is a filler cell; don't use a <th>, it'll confuse screen readers.
     884                      $fillercell->header = false;
     885                      $headingrow->cells[] = $fillercell;
     886                  } else if ($type == 'category') {
     887                      // Make sure the grade category has a grade total or at least has child grade items.
     888                      if (grade_tree::can_output_item($element)) {
     889                          // Element is a category.
     890                          $categorycell = new html_table_cell();
     891                          $categorycell->attributes['class'] = 'category ' . $catlevel;
     892                          $categorycell->colspan = $colspan;
     893                          $categorycell->text = $this->get_course_header($element);
     894                          $categorycell->header = true;
     895                          $categorycell->scope = 'col';
     896  
     897                          // Print icons.
     898                          if ($USER->gradeediting[$this->courseid]) {
     899                              $categorycell->text .= $this->get_icons($element);
     900                          }
     901  
     902                          $headingrow->cells[] = $categorycell;
     903                      }
     904                  } else {
     905                      // Element is a grade_item
     906                      if ($element['object']->id == $this->sortitemid) {
     907                          if ($this->sortorder == 'ASC') {
     908                              $arrow = $this->get_sort_arrow('up', $sortlink);
     909                          } else {
     910                              $arrow = $this->get_sort_arrow('down', $sortlink);
     911                          }
     912                      } else {
     913                          $arrow = $this->get_sort_arrow('move', $sortlink);
     914                      }
     915  
     916                      $headerlink = $this->gtree->get_element_header($element, true, $showactivityicons, false, false, true);
     917  
     918                      $itemcell = new html_table_cell();
     919                      $itemcell->attributes['class'] = $type . ' ' . $catlevel . ' highlightable'. ' i'. $element['object']->id;
     920                      $itemcell->attributes['data-itemid'] = $element['object']->id;
     921  
     922                      if ($element['object']->is_hidden()) {
     923                          $itemcell->attributes['class'] .= ' dimmed_text';
     924                      }
     925  
     926                      $singleview = '';
     927  
     928                      // FIXME: MDL-52678 This is extremely hacky we should have an API for inserting grade column links.
     929                      if (get_capability_info('gradereport/singleview:view')) {
     930                          if (has_all_capabilities(array('gradereport/singleview:view', 'moodle/grade:viewall',
     931                              'moodle/grade:edit'), $this->context)) {
     932  
     933                              $strsingleview = get_string('singleview', 'grades', $element['object']->get_name());
     934                              $url = new moodle_url('/grade/report/singleview/index.php', array(
     935                                  'id' => $this->course->id,
     936                                  'item' => 'grade',
     937                                  'itemid' => $element['object']->id));
     938                              $singleview = $OUTPUT->action_icon(
     939                                      $url,
     940                                      new pix_icon('t/editstring', ''),
     941                                      null,
     942                                      ['title' => $strsingleview, 'aria-label' => $strsingleview]
     943                              );
     944                          }
     945                      }
     946  
     947                      $itemcell->colspan = $colspan;
     948                      $itemcell->text = $headerlink . $arrow . $singleview;
     949                      $itemcell->header = true;
     950                      $itemcell->scope = 'col';
     951  
     952                      $headingrow->cells[] = $itemcell;
     953                  }
     954              }
     955              $rows[] = $headingrow;
     956          }
     957  
     958          $rows = $this->get_right_icons_row($rows);
     959  
     960          // Preload scale objects for items with a scaleid and initialize tab indices
     961          $scaleslist = array();
     962          $tabindices = array();
     963  
     964          foreach ($this->gtree->get_items() as $itemid => $item) {
     965              $scale = null;
     966              if (!empty($item->scaleid)) {
     967                  $scaleslist[] = $item->scaleid;
     968                  $jsarguments['items'][$itemid] = array('id'=>$itemid, 'name'=>$item->get_name(true), 'type'=>'scale', 'scale'=>$item->scaleid, 'decimals'=>$item->get_decimals());
     969              } else {
     970                  $jsarguments['items'][$itemid] = array('id'=>$itemid, 'name'=>$item->get_name(true), 'type'=>'value', 'scale'=>false, 'decimals'=>$item->get_decimals());
     971              }
     972              $tabindices[$item->id]['grade'] = $gradetabindex;
     973              $tabindices[$item->id]['feedback'] = $gradetabindex + $numusers;
     974              $gradetabindex += $numusers * 2;
     975          }
     976          $scalesarray = array();
     977  
     978          if (!empty($scaleslist)) {
     979              $scalesarray = $DB->get_records_list('scale', 'id', $scaleslist);
     980          }
     981          $jsscales = $scalesarray;
     982  
     983          // Get all the grade items if the user can not view hidden grade items.
     984          // It is possible that the user is simply viewing the 'Course total' by switching to the 'Aggregates only' view
     985          // and that this user does not have the ability to view hidden items. In this case we still need to pass all the
     986          // grade items (in case one has been hidden) as the course total shown needs to be adjusted for this particular
     987          // user.
     988          if (!$this->canviewhidden) {
     989              $allgradeitems = $this->get_allgradeitems();
     990          }
     991  
     992          foreach ($this->users as $userid => $user) {
     993  
     994              if ($this->canviewhidden) {
     995                  $altered = array();
     996                  $unknown = array();
     997              } else {
     998                  $usergrades = $this->allgrades[$userid];
     999                  $hidingaffected = grade_grade::get_hiding_affected($usergrades, $allgradeitems);
    1000                  $altered = $hidingaffected['altered'];
    1001                  $unknown = $hidingaffected['unknowngrades'];
    1002                  unset($hidingaffected);
    1003              }
    1004  
    1005              $itemrow = new html_table_row();
    1006              $itemrow->id = 'user_'.$userid;
    1007  
    1008              $fullname = fullname($user, $viewfullnames);
    1009              $jsarguments['users'][$userid] = $fullname;
    1010  
    1011              foreach ($this->gtree->items as $itemid => $unused) {
    1012                  $item =& $this->gtree->items[$itemid];
    1013                  $grade = $this->grades[$userid][$item->id];
    1014  
    1015                  $itemcell = new html_table_cell();
    1016  
    1017                  $itemcell->id = 'u'.$userid.'i'.$itemid;
    1018                  $itemcell->attributes['data-itemid'] = $itemid;
    1019  
    1020                  // Get the decimal points preference for this item
    1021                  $decimalpoints = $item->get_decimals();
    1022  
    1023                  if (array_key_exists($itemid, $unknown)) {
    1024                      $gradeval = null;
    1025                  } else if (array_key_exists($itemid, $altered)) {
    1026                      $gradeval = $altered[$itemid];
    1027                  } else {
    1028                      $gradeval = $grade->finalgrade;
    1029                  }
    1030                  if (!empty($grade->finalgrade)) {
    1031                      $gradevalforjs = null;
    1032                      if ($item->scaleid && !empty($scalesarray[$item->scaleid])) {
    1033                          $gradevalforjs = (int)$gradeval;
    1034                      } else {
    1035                          $gradevalforjs = format_float($gradeval, $decimalpoints);
    1036                      }
    1037                      $jsarguments['grades'][] = array('user'=>$userid, 'item'=>$itemid, 'grade'=>$gradevalforjs);
    1038                  }
    1039  
    1040                  // MDL-11274
    1041                  // Hide grades in the grader report if the current grader doesn't have 'moodle/grade:viewhidden'
    1042                  if (!$this->canviewhidden and $grade->is_hidden()) {
    1043                      if (!empty($CFG->grade_hiddenasdate) and $grade->get_datesubmitted() and !$item->is_category_item() and !$item->is_course_item()) {
    1044                          // the problem here is that we do not have the time when grade value was modified, 'timemodified' is general modification date for grade_grades records
    1045                          $itemcell->text = "<span class='datesubmitted'>" .
    1046                                  userdate($grade->get_datesubmitted(), $strftimedatetimeshort) . "</span>";
    1047                      } else {
    1048                          $itemcell->text = '-';
    1049                      }
    1050                      $itemrow->cells[] = $itemcell;
    1051                      continue;
    1052                  }
    1053  
    1054                  // emulate grade element
    1055                  $eid = $this->gtree->get_grade_eid($grade);
    1056                  $element = array('eid'=>$eid, 'object'=>$grade, 'type'=>'grade');
    1057  
    1058                  $itemcell->attributes['class'] .= ' grade i'.$itemid;
    1059                  if ($item->is_category_item()) {
    1060                      $itemcell->attributes['class'] .= ' cat';
    1061                  }
    1062                  if ($item->is_course_item()) {
    1063                      $itemcell->attributes['class'] .= ' course';
    1064                  }
    1065                  if ($grade->is_overridden()) {
    1066                      $itemcell->attributes['class'] .= ' overridden';
    1067                      $itemcell->attributes['aria-label'] = get_string('overriddengrade', 'gradereport_grader');
    1068                  }
    1069  
    1070                  if (!empty($grade->feedback)) {
    1071                      $feedback = wordwrap(trim(format_string($grade->feedback, $grade->feedbackformat)), 34, '<br>');
    1072                      $itemcell->attributes['data-feedback'] = $feedback;
    1073                      $jsarguments['feedback'][] = array('user'=>$userid, 'item'=>$itemid, 'content' => $feedback);
    1074                  }
    1075  
    1076                  if ($grade->is_excluded()) {
    1077                      // Adding white spaces before and after to prevent a screenreader from
    1078                      // thinking that the words are attached to the next/previous <span> or text.
    1079                      $itemcell->text .= " <span class='excludedfloater'>" . $strexcludedgrades . "</span> ";
    1080                  }
    1081  
    1082                  // Do not show any icons if no grade (no record in DB to match)
    1083                  if (!$item->needsupdate and $USER->gradeediting[$this->courseid]) {
    1084                      $itemcell->text .= $this->get_icons($element);
    1085                  }
    1086  
    1087                  $hidden = '';
    1088                  if ($grade->is_hidden()) {
    1089                      $hidden = ' dimmed_text ';
    1090                  }
    1091  
    1092                  $gradepass = ' gradefail ';
    1093                  $gradepassicon = $OUTPUT->pix_icon('i/invalid', get_string('fail', 'grades'));
    1094                  if ($grade->is_passed($item)) {
    1095                      $gradepass = ' gradepass ';
    1096                      $gradepassicon = $OUTPUT->pix_icon('i/valid', get_string('pass', 'grades'));
    1097                  } else if (is_null($grade->is_passed($item))) {
    1098                      $gradepass = '';
    1099                      $gradepassicon = '';
    1100                  }
    1101  
    1102                  // if in editing mode, we need to print either a text box
    1103                  // or a drop down (for scales)
    1104                  // grades in item of type grade category or course are not directly editable
    1105                  if ($item->needsupdate) {
    1106                      $itemcell->text .= "<span class='gradingerror{$hidden}'>" . $strerror . "</span>";
    1107  
    1108                  } else if ($USER->gradeediting[$this->courseid]) {
    1109  
    1110                      if ($item->scaleid && !empty($scalesarray[$item->scaleid])) {
    1111                          $itemcell->attributes['class'] .= ' grade_type_scale';
    1112                      } else if ($item->gradetype == GRADE_TYPE_VALUE) {
    1113                          $itemcell->attributes['class'] .= ' grade_type_value';
    1114                      } else if ($item->gradetype == GRADE_TYPE_TEXT) {
    1115                          $itemcell->attributes['class'] .= ' grade_type_text';
    1116                      }
    1117  
    1118                      if ($item->scaleid && !empty($scalesarray[$item->scaleid])) {
    1119                          $scale = $scalesarray[$item->scaleid];
    1120                          $gradeval = (int)$gradeval; // scales use only integers
    1121                          $scales = explode(",", $scale->scale);
    1122                          // reindex because scale is off 1
    1123  
    1124                          // MDL-12104 some previous scales might have taken up part of the array
    1125                          // so this needs to be reset
    1126                          $scaleopt = array();
    1127                          $i = 0;
    1128                          foreach ($scales as $scaleoption) {
    1129                              $i++;
    1130                              $scaleopt[$i] = $scaleoption;
    1131                          }
    1132  
    1133                          if ($quickgrading and $grade->is_editable()) {
    1134                              $oldval = empty($gradeval) ? -1 : $gradeval;
    1135                              if (empty($item->outcomeid)) {
    1136                                  $nogradestr = $this->get_lang_string('nograde');
    1137                              } else {
    1138                                  $nogradestr = $this->get_lang_string('nooutcome', 'grades');
    1139                              }
    1140                              $attributes = array('tabindex' => $tabindices[$item->id]['grade'], 'id'=>'grade_'.$userid.'_'.$item->id);
    1141                              $gradelabel = $fullname . ' ' . $item->get_name(true);
    1142                              $itemcell->text .= html_writer::label(
    1143                                  get_string('useractivitygrade', 'gradereport_grader', $gradelabel), $attributes['id'], false,
    1144                                      array('class' => 'accesshide'));
    1145                              $itemcell->text .= html_writer::select($scaleopt, 'grade['.$userid.']['.$item->id.']', $gradeval, array(-1=>$nogradestr), $attributes);
    1146                          } else if (!empty($scale)) {
    1147                              $scales = explode(",", $scale->scale);
    1148  
    1149                              // invalid grade if gradeval < 1
    1150                              if ($gradeval < 1) {
    1151                                  $itemcell->text .= $gradepassicon .
    1152                                      "<span class='gradevalue{$hidden}{$gradepass}'>-</span>";
    1153                              } else {
    1154                                  $gradeval = $grade->grade_item->bounded_grade($gradeval); //just in case somebody changes scale
    1155                                  $itemcell->text .= $gradepassicon .
    1156                                      "<span class='gradevalue{$hidden}{$gradepass}'>{$scales[$gradeval - 1]}</span>";
    1157                              }
    1158                          }
    1159  
    1160                      } else if ($item->gradetype != GRADE_TYPE_TEXT) { // Value type
    1161                          if ($quickgrading and $grade->is_editable()) {
    1162                              $value = format_float($gradeval, $decimalpoints);
    1163                              $gradelabel = $fullname . ' ' . $item->get_name(true);
    1164                              $itemcell->text .= '<label class="accesshide" for="grade_'.$userid.'_'.$item->id.'">'
    1165                                            .get_string('useractivitygrade', 'gradereport_grader', $gradelabel).'</label>';
    1166                              $itemcell->text .= '<input size="6" tabindex="' . $tabindices[$item->id]['grade']
    1167                                            . '" type="text" class="text" title="'. $strgrade .'" name="grade['
    1168                                            .$userid.'][' .$item->id.']" id="grade_'.$userid.'_'.$item->id.'" value="'.$value.'" />';
    1169                          } else {
    1170                              $itemcell->text .= $gradepassicon . "<span class='gradevalue{$hidden}{$gradepass}'>" .
    1171                                      format_float($gradeval, $decimalpoints) . "</span>";
    1172                          }
    1173                      }
    1174  
    1175                      // If quickfeedback is on, print an input element
    1176                      if ($showquickfeedback and $grade->is_editable()) {
    1177                          $feedbacklabel = $fullname . ' ' . $item->get_name(true);
    1178                          $itemcell->text .= '<label class="accesshide" for="feedback_'.$userid.'_'.$item->id.'">'
    1179                                        .get_string('useractivityfeedback', 'gradereport_grader', $feedbacklabel).'</label>';
    1180                          $itemcell->text .= '<input class="quickfeedback" tabindex="' . $tabindices[$item->id]['feedback'].'" id="feedback_'.$userid.'_'.$item->id
    1181                                        . '" size="6" title="' . $strfeedback . '" type="text" name="feedback['.$userid.']['.$item->id.']" value="' . s($grade->feedback) . '" />';
    1182                      }
    1183  
    1184                  } else { // Not editing
    1185                      $gradedisplaytype = $item->get_displaytype();
    1186  
    1187                      if ($item->scaleid && !empty($scalesarray[$item->scaleid])) {
    1188                          $itemcell->attributes['class'] .= ' grade_type_scale';
    1189                      } else if ($item->gradetype == GRADE_TYPE_VALUE) {
    1190                          $itemcell->attributes['class'] .= ' grade_type_value';
    1191                      } else if ($item->gradetype == GRADE_TYPE_TEXT) {
    1192                          $itemcell->attributes['class'] .= ' grade_type_text';
    1193                      }
    1194  
    1195                      // Only allow edting if the grade is editable (not locked, not in a unoverridable category, etc).
    1196                      if ($enableajax && $grade->is_editable()) {
    1197                          // If a grade item is type text, and we don't have show quick feedback on, it can't be edited.
    1198                          if ($item->gradetype != GRADE_TYPE_TEXT || $showquickfeedback) {
    1199                              $itemcell->attributes['class'] .= ' clickable';
    1200                          }
    1201                      }
    1202  
    1203                      if ($item->needsupdate) {
    1204                          $itemcell->text .= $gradepassicon . "<span class='gradingerror{$hidden}{$gradepass}'>" . $error . "</span>";
    1205                      } else {
    1206                          // The max and min for an aggregation may be different to the grade_item.
    1207                          if (!is_null($gradeval)) {
    1208                              $item->grademax = $grade->get_grade_max();
    1209                              $item->grademin = $grade->get_grade_min();
    1210                          }
    1211  
    1212                          $itemcell->text .= $gradepassicon . "<span class='gradevalue{$hidden}{$gradepass}'>" .
    1213                                  grade_format_gradevalue($gradeval, $item, true, $gradedisplaytype, null) . "</span>";
    1214                          if ($showanalysisicon) {
    1215                              $itemcell->text .= $this->gtree->get_grade_analysis_icon($grade);
    1216                          }
    1217                      }
    1218                  }
    1219  
    1220                  // Enable keyboard navigation if the grade is editable (not locked, not in a unoverridable category, etc).
    1221                  if ($enableajax && $grade->is_editable()) {
    1222                      // If a grade item is type text, and we don't have show quick feedback on, it can't be edited.
    1223                      if ($item->gradetype != GRADE_TYPE_TEXT || $showquickfeedback) {
    1224                          $itemcell->attributes['class'] .= ' gbnavigable';
    1225                      }
    1226                  }
    1227  
    1228                  if (!empty($this->gradeserror[$item->id][$userid])) {
    1229                      $itemcell->text .= $this->gradeserror[$item->id][$userid];
    1230                  }
    1231  
    1232                  $itemrow->cells[] = $itemcell;
    1233              }
    1234              $rows[] = $itemrow;
    1235          }
    1236  
    1237          if ($enableajax) {
    1238              $jsarguments['cfg']['ajaxenabled'] = true;
    1239              $jsarguments['cfg']['scales'] = array();
    1240              foreach ($jsscales as $scale) {
    1241                  // Trim the scale values, as they may have a space that is ommitted from values later.
    1242                  $jsarguments['cfg']['scales'][$scale->id] = array_map('trim', explode(',', $scale->scale));
    1243              }
    1244              $jsarguments['cfg']['feedbacktrunclength'] =  $this->feedback_trunc_length;
    1245  
    1246              // Student grades and feedback are already at $jsarguments['feedback'] and $jsarguments['grades']
    1247          }
    1248          $jsarguments['cfg']['isediting'] = (bool)$USER->gradeediting[$this->courseid];
    1249          $jsarguments['cfg']['courseid'] = $this->courseid;
    1250          $jsarguments['cfg']['studentsperpage'] = $this->get_students_per_page();
    1251          $jsarguments['cfg']['showquickfeedback'] = (bool) $showquickfeedback;
    1252  
    1253          $module = array(
    1254              'name'      => 'gradereport_grader',
    1255              'fullpath'  => '/grade/report/grader/module.js',
    1256              'requires'  => array('base', 'dom', 'event', 'event-mouseenter', 'event-key', 'io-queue', 'json-parse', 'overlay')
    1257          );
    1258          $PAGE->requires->js_init_call('M.gradereport_grader.init_report', $jsarguments, false, $module);
    1259          $PAGE->requires->strings_for_js(array('addfeedback', 'feedback', 'grade'), 'grades');
    1260          $PAGE->requires->strings_for_js(array('ajaxchoosescale', 'ajaxclicktoclose', 'ajaxerror', 'ajaxfailedupdate', 'ajaxfieldchanged'), 'gradereport_grader');
    1261          if (!$enableajax && $USER->gradeediting[$this->courseid]) {
    1262              $PAGE->requires->yui_module('moodle-core-formchangechecker',
    1263                      'M.core_formchangechecker.init',
    1264                      array(array(
    1265                          'formid' => 'gradereport_grader'
    1266                      ))
    1267              );
    1268              $PAGE->requires->string_for_js('changesmadereallygoaway', 'moodle');
    1269          }
    1270  
    1271          $rows = $this->get_right_range_row($rows);
    1272          if ($displayaverages) {
    1273              $rows = $this->get_right_avg_row($rows, true);
    1274              $rows = $this->get_right_avg_row($rows);
    1275          }
    1276  
    1277          return $rows;
    1278      }
    1279  
    1280      /**
    1281       * Depending on the style of report (fixedstudents vs traditional one-table),
    1282       * arranges the rows of data in one or two tables, and returns the output of
    1283       * these tables in HTML
    1284       * @param boolean $displayaverages whether to display average rows in the table
    1285       * @return string HTML
    1286       */
    1287      public function get_grade_table($displayaverages = false) {
    1288          global $OUTPUT;
    1289          $leftrows = $this->get_left_rows($displayaverages);
    1290          $rightrows = $this->get_right_rows($displayaverages);
    1291  
    1292          $html = '';
    1293  
    1294          $fulltable = new html_table();
    1295          $fulltable->attributes['class'] = 'gradereport-grader-table';
    1296          $fulltable->id = 'user-grades';
    1297          $fulltable->caption = get_string('summarygrader', 'gradereport_grader');
    1298          $fulltable->captionhide = true;
    1299  
    1300          // Extract rows from each side (left and right) and collate them into one row each
    1301          foreach ($leftrows as $key => $row) {
    1302              $row->cells = array_merge($row->cells, $rightrows[$key]->cells);
    1303              $fulltable->data[] = $row;
    1304          }
    1305          $html .= html_writer::table($fulltable);
    1306          return $OUTPUT->container($html, 'gradeparent');
    1307      }
    1308  
    1309      /**
    1310       * Builds and return the row of icons for the left side of the report.
    1311       * It only has one cell that says "Controls"
    1312       * @param array $rows The Array of rows for the left part of the report
    1313       * @param int $colspan The number of columns this cell has to span
    1314       * @return array Array of rows for the left part of the report
    1315       */
    1316      public function get_left_icons_row($rows=array(), $colspan=1) {
    1317          global $USER;
    1318  
    1319          if ($USER->gradeediting[$this->courseid]) {
    1320              $controlsrow = new html_table_row();
    1321              $controlsrow->attributes['class'] = 'controls';
    1322              $controlscell = new html_table_cell();
    1323              $controlscell->attributes['class'] = 'header controls';
    1324              $controlscell->colspan = $colspan;
    1325              $controlscell->text = $this->get_lang_string('controls', 'grades');
    1326  
    1327              $controlsrow->cells[] = $controlscell;
    1328              $rows[] = $controlsrow;
    1329          }
    1330          return $rows;
    1331      }
    1332  
    1333      /**
    1334       * Builds and return the header for the row of ranges, for the left part of the grader report.
    1335       * @param array $rows The Array of rows for the left part of the report
    1336       * @param int $colspan The number of columns this cell has to span
    1337       * @return array Array of rows for the left part of the report
    1338       */
    1339      public function get_left_range_row($rows=array(), $colspan=1) {
    1340          global $CFG, $USER;
    1341  
    1342          if ($this->get_pref('showranges')) {
    1343              $rangerow = new html_table_row();
    1344              $rangerow->attributes['class'] = 'range r'.$this->rowcount++;
    1345              $rangecell = new html_table_cell();
    1346              $rangecell->attributes['class'] = 'header range';
    1347              $rangecell->colspan = $colspan;
    1348              $rangecell->header = true;
    1349              $rangecell->scope = 'row';
    1350              $rangecell->text = $this->get_lang_string('range', 'grades');
    1351              $rangerow->cells[] = $rangecell;
    1352              $rows[] = $rangerow;
    1353          }
    1354  
    1355          return $rows;
    1356      }
    1357  
    1358      /**
    1359       * Builds and return the headers for the rows of averages, for the left part of the grader report.
    1360       * @param array $rows The Array of rows for the left part of the report
    1361       * @param int $colspan The number of columns this cell has to span
    1362       * @param bool $groupavg If true, returns the row for group averages, otherwise for overall averages
    1363       * @return array Array of rows for the left part of the report
    1364       */
    1365      public function get_left_avg_row($rows=array(), $colspan=1, $groupavg=false) {
    1366          if (!$this->canviewhidden) {
    1367              // totals might be affected by hiding, if user can not see hidden grades the aggregations might be altered
    1368              // better not show them at all if user can not see all hideen grades
    1369              return $rows;
    1370          }
    1371  
    1372          $showaverages = $this->get_pref('showaverages');
    1373          $showaveragesgroup = $this->currentgroup && $showaverages;
    1374          $straveragegroup = get_string('groupavg', 'grades');
    1375  
    1376          if ($groupavg) {
    1377              if ($showaveragesgroup) {
    1378                  $groupavgrow = new html_table_row();
    1379                  $groupavgrow->attributes['class'] = 'groupavg r'.$this->rowcount++;
    1380                  $groupavgcell = new html_table_cell();
    1381                  $groupavgcell->attributes['class'] = 'header range';
    1382                  $groupavgcell->colspan = $colspan;
    1383                  $groupavgcell->header = true;
    1384                  $groupavgcell->scope = 'row';
    1385                  $groupavgcell->text = $straveragegroup;
    1386                  $groupavgrow->cells[] = $groupavgcell;
    1387                  $rows[] = $groupavgrow;
    1388              }
    1389          } else {
    1390              $straverage = get_string('overallaverage', 'grades');
    1391  
    1392              if ($showaverages) {
    1393                  $avgrow = new html_table_row();
    1394                  $avgrow->attributes['class'] = 'avg r'.$this->rowcount++;
    1395                  $avgcell = new html_table_cell();
    1396                  $avgcell->attributes['class'] = 'header range';
    1397                  $avgcell->colspan = $colspan;
    1398                  $avgcell->header = true;
    1399                  $avgcell->scope = 'row';
    1400                  $avgcell->text = $straverage;
    1401                  $avgrow->cells[] = $avgcell;
    1402                  $rows[] = $avgrow;
    1403              }
    1404          }
    1405  
    1406          return $rows;
    1407      }
    1408  
    1409      /**
    1410       * Builds and return the row of icons when editing is on, for the right part of the grader report.
    1411       * @param array $rows The Array of rows for the right part of the report
    1412       * @return array Array of rows for the right part of the report
    1413       */
    1414      public function get_right_icons_row($rows=array()) {
    1415          global $USER;
    1416          if ($USER->gradeediting[$this->courseid]) {
    1417              $iconsrow = new html_table_row();
    1418              $iconsrow->attributes['class'] = 'controls';
    1419  
    1420              foreach ($this->gtree->items as $itemid => $unused) {
    1421                  // emulate grade element
    1422                  $item = $this->gtree->get_item($itemid);
    1423  
    1424                  $eid = $this->gtree->get_item_eid($item);
    1425                  $element = $this->gtree->locate_element($eid);
    1426                  $itemcell = new html_table_cell();
    1427                  $itemcell->attributes['class'] = 'controls icons i'.$itemid;
    1428                  $itemcell->text = $this->get_icons($element);
    1429                  $iconsrow->cells[] = $itemcell;
    1430              }
    1431              $rows[] = $iconsrow;
    1432          }
    1433          return $rows;
    1434      }
    1435  
    1436      /**
    1437       * Builds and return the row of ranges for the right part of the grader report.
    1438       * @param array $rows The Array of rows for the right part of the report
    1439       * @return array Array of rows for the right part of the report
    1440       */
    1441      public function get_right_range_row($rows=array()) {
    1442          global $OUTPUT;
    1443  
    1444          if ($this->get_pref('showranges')) {
    1445              $rangesdisplaytype   = $this->get_pref('rangesdisplaytype');
    1446              $rangesdecimalpoints = $this->get_pref('rangesdecimalpoints');
    1447              $rangerow = new html_table_row();
    1448              $rangerow->attributes['class'] = 'heading range';
    1449  
    1450              foreach ($this->gtree->items as $itemid => $unused) {
    1451                  $item =& $this->gtree->items[$itemid];
    1452                  $itemcell = new html_table_cell();
    1453                  $itemcell->attributes['class'] .= ' range i'. $itemid;
    1454  
    1455                  $hidden = '';
    1456                  if ($item->is_hidden()) {
    1457                      $hidden = ' dimmed_text ';
    1458                  }
    1459  
    1460                  $formattedrange = $item->get_formatted_range($rangesdisplaytype, $rangesdecimalpoints);
    1461  
    1462                  $itemcell->text = $OUTPUT->container($formattedrange, 'rangevalues'.$hidden);
    1463                  $rangerow->cells[] = $itemcell;
    1464              }
    1465              $rows[] = $rangerow;
    1466          }
    1467          return $rows;
    1468      }
    1469  
    1470      /**
    1471       * Builds and return the row of averages for the right part of the grader report.
    1472       * @param array $rows Whether to return only group averages or all averages.
    1473       * @param bool $grouponly Whether to return only group averages or all averages.
    1474       * @return array Array of rows for the right part of the report
    1475       */
    1476      public function get_right_avg_row($rows=array(), $grouponly=false) {
    1477          global $USER, $DB, $OUTPUT, $CFG;
    1478  
    1479          if (!$this->canviewhidden) {
    1480              // Totals might be affected by hiding, if user can not see hidden grades the aggregations might be altered
    1481              // better not show them at all if user can not see all hidden grades.
    1482              return $rows;
    1483          }
    1484  
    1485          $averagesdisplaytype   = $this->get_pref('averagesdisplaytype');
    1486          $averagesdecimalpoints = $this->get_pref('averagesdecimalpoints');
    1487          $meanselection         = $this->get_pref('meanselection');
    1488          $shownumberofgrades    = $this->get_pref('shownumberofgrades');
    1489  
    1490          if ($grouponly) {
    1491              $showaverages = $this->currentgroup && $this->get_pref('showaverages');
    1492              $groupsql = $this->groupsql;
    1493              $groupwheresql = $this->groupwheresql;
    1494              $groupwheresqlparams = $this->groupwheresql_params;
    1495          } else {
    1496              $showaverages = $this->get_pref('showaverages');
    1497              $groupsql = "";
    1498              $groupwheresql = "";
    1499              $groupwheresqlparams = array();
    1500          }
    1501  
    1502          if ($showaverages) {
    1503              $totalcount = $this->get_numusers($grouponly);
    1504  
    1505              // Limit to users with a gradeable role.
    1506              list($gradebookrolessql, $gradebookrolesparams) = $DB->get_in_or_equal(explode(',', $this->gradebookroles), SQL_PARAMS_NAMED, 'grbr0');
    1507  
    1508              // Limit to users with an active enrollment.
    1509              $coursecontext = $this->context->get_course_context(true);
    1510              $defaultgradeshowactiveenrol = !empty($CFG->grade_report_showonlyactiveenrol);
    1511              $showonlyactiveenrol = get_user_preferences('grade_report_showonlyactiveenrol', $defaultgradeshowactiveenrol);
    1512              $showonlyactiveenrol = $showonlyactiveenrol || !has_capability('moodle/course:viewsuspendedusers', $coursecontext);
    1513              list($enrolledsql, $enrolledparams) = get_enrolled_sql($this->context, '', 0, $showonlyactiveenrol);
    1514  
    1515              // We want to query both the current context and parent contexts.
    1516              list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($this->context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx');
    1517  
    1518              $params = array_merge(array('courseid' => $this->courseid), $gradebookrolesparams, $enrolledparams, $groupwheresqlparams, $relatedctxparams);
    1519  
    1520              // Find sums of all grade items in course.
    1521              $sql = "SELECT g.itemid, SUM(g.finalgrade) AS sum
    1522                        FROM {grade_items} gi
    1523                        JOIN {grade_grades} g ON g.itemid = gi.id
    1524                        JOIN {user} u ON u.id = g.userid
    1525                        JOIN ($enrolledsql) je ON je.id = u.id
    1526                        JOIN (
    1527                                 SELECT DISTINCT ra.userid
    1528                                   FROM {role_assignments} ra
    1529                                  WHERE ra.roleid $gradebookrolessql
    1530                                    AND ra.contextid $relatedctxsql
    1531                             ) rainner ON rainner.userid = u.id
    1532                        $groupsql
    1533                       WHERE gi.courseid = :courseid
    1534                         AND u.deleted = 0
    1535                         AND g.finalgrade IS NOT NULL
    1536                         $groupwheresql
    1537                       GROUP BY g.itemid";
    1538              $sumarray = array();
    1539              if ($sums = $DB->get_records_sql($sql, $params)) {
    1540                  foreach ($sums as $itemid => $csum) {
    1541                      $sumarray[$itemid] = $csum->sum;
    1542                  }
    1543              }
    1544  
    1545              // MDL-10875 Empty grades must be evaluated as grademin, NOT always 0
    1546              // This query returns a count of ungraded grades (NULL finalgrade OR no matching record in grade_grades table)
    1547              $sql = "SELECT gi.id, COUNT(DISTINCT u.id) AS count
    1548                        FROM {grade_items} gi
    1549                        CROSS JOIN ($enrolledsql) u
    1550                        JOIN {role_assignments} ra
    1551                             ON ra.userid = u.id
    1552                        LEFT OUTER JOIN {grade_grades} g
    1553                             ON (g.itemid = gi.id AND g.userid = u.id AND g.finalgrade IS NOT NULL)
    1554                        $groupsql
    1555                       WHERE gi.courseid = :courseid
    1556                             AND ra.roleid $gradebookrolessql
    1557                             AND ra.contextid $relatedctxsql
    1558                             AND g.id IS NULL
    1559                             $groupwheresql
    1560                    GROUP BY gi.id";
    1561  
    1562              $ungradedcounts = $DB->get_records_sql($sql, $params);
    1563  
    1564              $avgrow = new html_table_row();
    1565              $avgrow->attributes['class'] = 'avg';
    1566  
    1567              foreach ($this->gtree->items as $itemid => $unused) {
    1568                  $item =& $this->gtree->items[$itemid];
    1569  
    1570                  if ($item->needsupdate) {
    1571                      $avgcell = new html_table_cell();
    1572                      $avgcell->attributes['class'] = 'i'. $itemid;
    1573                      $avgcell->text = $OUTPUT->container(get_string('error'), 'gradingerror');
    1574                      $avgrow->cells[] = $avgcell;
    1575                      continue;
    1576                  }
    1577  
    1578                  if (!isset($sumarray[$item->id])) {
    1579                      $sumarray[$item->id] = 0;
    1580                  }
    1581  
    1582                  if (empty($ungradedcounts[$itemid])) {
    1583                      $ungradedcount = 0;
    1584                  } else {
    1585                      $ungradedcount = $ungradedcounts[$itemid]->count;
    1586                  }
    1587  
    1588                  if ($meanselection == GRADE_REPORT_MEAN_GRADED) {
    1589                      $meancount = $totalcount - $ungradedcount;
    1590                  } else { // Bump up the sum by the number of ungraded items * grademin
    1591                      $sumarray[$item->id] += $ungradedcount * $item->grademin;
    1592                      $meancount = $totalcount;
    1593                  }
    1594  
    1595                  // Determine which display type to use for this average
    1596                  if ($USER->gradeediting[$this->courseid]) {
    1597                      $displaytype = GRADE_DISPLAY_TYPE_REAL;
    1598  
    1599                  } else if ($averagesdisplaytype == GRADE_REPORT_PREFERENCE_INHERIT) { // no ==0 here, please resave the report and user preferences
    1600                      $displaytype = $item->get_displaytype();
    1601  
    1602                  } else {
    1603                      $displaytype = $averagesdisplaytype;
    1604                  }
    1605  
    1606                  // Override grade_item setting if a display preference (not inherit) was set for the averages
    1607                  if ($averagesdecimalpoints == GRADE_REPORT_PREFERENCE_INHERIT) {
    1608                      $decimalpoints = $item->get_decimals();
    1609  
    1610                  } else {
    1611                      $decimalpoints = $averagesdecimalpoints;
    1612                  }
    1613  
    1614                  if (!isset($sumarray[$item->id]) || $meancount == 0) {
    1615                      $avgcell = new html_table_cell();
    1616                      $avgcell->attributes['class'] = 'i'. $itemid;
    1617                      $avgcell->text = '-';
    1618                      $avgrow->cells[] = $avgcell;
    1619  
    1620                  } else {
    1621                      $sum = $sumarray[$item->id];
    1622                      $avgradeval = $sum/$meancount;
    1623                      $gradehtml = grade_format_gradevalue($avgradeval, $item, true, $displaytype, $decimalpoints);
    1624  
    1625                      $numberofgrades = '';
    1626                      if ($shownumberofgrades) {
    1627                          $numberofgrades = " ($meancount)";
    1628                      }
    1629  
    1630                      $avgcell = new html_table_cell();
    1631                      $avgcell->attributes['class'] = 'i'. $itemid;
    1632                      $avgcell->text = $gradehtml.$numberofgrades;
    1633                      $avgrow->cells[] = $avgcell;
    1634                  }
    1635              }
    1636              $rows[] = $avgrow;
    1637          }
    1638          return $rows;
    1639      }
    1640  
    1641      /**
    1642       * Given element category, create a collapsible icon and
    1643       * course header.
    1644       *
    1645       * @param array $element
    1646       * @return string HTML
    1647       */
    1648      protected function get_course_header($element) {
    1649          global $OUTPUT;
    1650  
    1651          $icon = '';
    1652          // If object is a category, display expand/contract icon.
    1653          if ($element['type'] == 'category') {
    1654              // Load language strings.
    1655              $strswitchminus = $this->get_lang_string('aggregatesonly', 'grades');
    1656              $strswitchplus  = $this->get_lang_string('gradesonly', 'grades');
    1657              $strswitchwhole = $this->get_lang_string('fullmode', 'grades');
    1658  
    1659              $url = new moodle_url($this->gpr->get_return_url(null, array('target' => $element['eid'], 'sesskey' => sesskey())));
    1660  
    1661              if (in_array($element['object']->id, $this->collapsed['aggregatesonly'])) {
    1662                  $url->param('action', 'switch_plus');
    1663                  $icon = $OUTPUT->action_icon($url, new pix_icon('t/switch_plus', ''), null,
    1664                          ['title' => $strswitchplus, 'aria-label' => $strswitchplus]);
    1665                  $showing = get_string('showingaggregatesonly', 'grades');
    1666              } else if (in_array($element['object']->id, $this->collapsed['gradesonly'])) {
    1667                  $url->param('action', 'switch_whole');
    1668                  $icon = $OUTPUT->action_icon($url, new pix_icon('t/switch_whole', ''), null,
    1669                          ['title' => $strswitchwhole, 'aria-label' => $strswitchwhole]);
    1670                  $showing = get_string('showinggradesonly', 'grades');
    1671              } else {
    1672                  $url->param('action', 'switch_minus');
    1673                  $icon = $OUTPUT->action_icon($url, new pix_icon('t/switch_minus', ''), null,
    1674                          ['title' => $strswitchminus, 'aria-label' => $strswitchminus]);
    1675                  $showing = get_string('showingfullmode', 'grades');
    1676              }
    1677          }
    1678  
    1679          $name = $element['object']->get_name();
    1680          $nameunescaped = $element['object']->get_name(false);
    1681          $describedbyid = uniqid();
    1682          $courseheader = html_writer::tag('span', $name, [
    1683              'title' => $nameunescaped,
    1684              'class' => 'gradeitemheader',
    1685              'aria-describedby' => $describedbyid
    1686          ]);
    1687          $courseheader .= html_writer::div($showing, 'sr-only', [
    1688              'id' => $describedbyid
    1689          ]);
    1690          $courseheader .= $icon;
    1691  
    1692          return $courseheader;
    1693      }
    1694  
    1695      /**
    1696       * Given a grade_category, grade_item or grade_grade, this function
    1697       * figures out the state of the object and builds then returns a div
    1698       * with the icons needed for the grader report.
    1699       *
    1700       * @param array $element
    1701       * @return string HTML
    1702       */
    1703      protected function get_icons($element) {
    1704          global $CFG, $USER, $OUTPUT;
    1705  
    1706          if (!$USER->gradeediting[$this->courseid]) {
    1707              return '<div class="grade_icons" />';
    1708          }
    1709  
    1710          // Init all icons
    1711          $editicon = '';
    1712  
    1713          $editable = true;
    1714  
    1715          if ($element['type'] == 'grade') {
    1716              $item = $element['object']->grade_item;
    1717              if ($item->is_course_item() or $item->is_category_item()) {
    1718                  $editable = $this->overridecat;
    1719              }
    1720          }
    1721  
    1722          if ($element['type'] != 'categoryitem' && $element['type'] != 'courseitem' && $editable) {
    1723              $editicon = $this->gtree->get_edit_icon($element, $this->gpr);
    1724          }
    1725  
    1726          $editcalculationicon = '';
    1727          $showhideicon        = '';
    1728          $lockunlockicon      = '';
    1729  
    1730          if (has_capability('moodle/grade:manage', $this->context)) {
    1731              if ($this->get_pref('showcalculations')) {
    1732                  $editcalculationicon = $this->gtree->get_calculation_icon($element, $this->gpr);
    1733              }
    1734  
    1735              if ($this->get_pref('showeyecons')) {
    1736                  $showhideicon = $this->gtree->get_hiding_icon($element, $this->gpr);
    1737              }
    1738  
    1739              if ($this->get_pref('showlocks')) {
    1740                  $lockunlockicon = $this->gtree->get_locking_icon($element, $this->gpr);
    1741              }
    1742  
    1743          }
    1744  
    1745          $gradeanalysisicon   = '';
    1746          if ($this->get_pref('showanalysisicon') && $element['type'] == 'grade') {
    1747              $gradeanalysisicon .= $this->gtree->get_grade_analysis_icon($element['object']);
    1748          }
    1749  
    1750          return $OUTPUT->container($editicon.$editcalculationicon.$showhideicon.$lockunlockicon.$gradeanalysisicon, 'grade_icons');
    1751      }
    1752  
    1753      /**
    1754       * Given a category element returns collapsing +/- icon if available
    1755       *
    1756       * @deprecated since Moodle 2.9 MDL-46662 - please do not use this function any more.
    1757       */
    1758      protected function get_collapsing_icon($element) {
    1759          throw new coding_exception('get_collapsing_icon() can not be used any more, please use get_course_header() instead.');
    1760      }
    1761  
    1762      /**
    1763       * Processes a single action against a category, grade_item or grade.
    1764       * @param string $target eid ({type}{id}, e.g. c4 for category4)
    1765       * @param string $action Which action to take (edit, delete etc...)
    1766       * @return
    1767       */
    1768      public function process_action($target, $action) {
    1769          return self::do_process_action($target, $action, $this->course->id);
    1770      }
    1771  
    1772      /**
    1773       * From the list of categories that this user prefers to collapse choose ones that belong to the current course.
    1774       *
    1775       * This function serves two purposes.
    1776       * Mainly it helps migrating from user preference style when all courses were stored in one preference.
    1777       * Also it helps to remove the settings for categories that were removed if the array for one course grows too big.
    1778       *
    1779       * @param int $courseid
    1780       * @param array $collapsed
    1781       * @return array
    1782       */
    1783      protected static function filter_collapsed_categories($courseid, $collapsed) {
    1784          global $DB;
    1785          // Ensure we always have an element for aggregatesonly and another for gradesonly, no matter it's empty.
    1786          $collapsed['aggregatesonly'] = $collapsed['aggregatesonly'] ?? [];
    1787          $collapsed['gradesonly'] = $collapsed['gradesonly'] ?? [];
    1788  
    1789          if (empty($collapsed['aggregatesonly']) && empty($collapsed['gradesonly'])) {
    1790              return $collapsed;
    1791          }
    1792          $cats = $DB->get_fieldset_select('grade_categories', 'id', 'courseid = ?', array($courseid));
    1793          $collapsed['aggregatesonly'] = array_values(array_intersect($collapsed['aggregatesonly'], $cats));
    1794          $collapsed['gradesonly'] = array_values(array_intersect($collapsed['gradesonly'], $cats));
    1795          return $collapsed;
    1796      }
    1797  
    1798      /**
    1799       * Returns the list of categories that this user wants to collapse or display aggregatesonly
    1800       *
    1801       * This method also migrates on request from the old format of storing user preferences when they were stored
    1802       * in one preference for all courses causing DB error when trying to insert very big value.
    1803       *
    1804       * @param int $courseid
    1805       * @return array
    1806       */
    1807      protected static function get_collapsed_preferences($courseid) {
    1808          if ($collapsed = get_user_preferences('grade_report_grader_collapsed_categories'.$courseid)) {
    1809              $collapsed = json_decode($collapsed, true);
    1810              // Ensure we always have an element for aggregatesonly and another for gradesonly, no matter it's empty.
    1811              $collapsed['aggregatesonly'] = $collapsed['aggregatesonly'] ?? [];
    1812              $collapsed['gradesonly'] = $collapsed['gradesonly'] ?? [];
    1813              return $collapsed;
    1814          }
    1815  
    1816          // Try looking for old location of user setting that used to store all courses in one serialized user preference.
    1817          $collapsed = ['aggregatesonly' => [], 'gradesonly' => []]; // Use this if old settings are not found.
    1818          $collapsedall = [];
    1819          $oldprefexists = false;
    1820          if (($oldcollapsedpref = get_user_preferences('grade_report_grader_collapsed_categories')) !== null) {
    1821              $oldprefexists = true;
    1822              if ($collapsedall = unserialize_array($oldcollapsedpref)) {
    1823                  // Ensure we always have an element for aggregatesonly and another for gradesonly, no matter it's empty.
    1824                  $collapsedall['aggregatesonly'] = $collapsedall['aggregatesonly'] ?? [];
    1825                  $collapsedall['gradesonly'] = $collapsedall['gradesonly'] ?? [];
    1826                  // We found the old-style preference, filter out only categories that belong to this course and update the prefs.
    1827                  $collapsed = static::filter_collapsed_categories($courseid, $collapsedall);
    1828                  if (!empty($collapsed['aggregatesonly']) || !empty($collapsed['gradesonly'])) {
    1829                      static::set_collapsed_preferences($courseid, $collapsed);
    1830                      $collapsedall['aggregatesonly'] = array_diff($collapsedall['aggregatesonly'], $collapsed['aggregatesonly']);
    1831                      $collapsedall['gradesonly'] = array_diff($collapsedall['gradesonly'], $collapsed['gradesonly']);
    1832                      if (!empty($collapsedall['aggregatesonly']) || !empty($collapsedall['gradesonly'])) {
    1833                          set_user_preference('grade_report_grader_collapsed_categories', serialize($collapsedall));
    1834                      }
    1835                  }
    1836              }
    1837          }
    1838  
    1839          // Arrived here, if the old pref exists and it doesn't contain
    1840          // more information, it means that the migration of all the
    1841          // data to new, by course, preferences is completed, so
    1842          // the old one can be safely deleted.
    1843          if ($oldprefexists &&
    1844                  empty($collapsedall['aggregatesonly']) &&
    1845                  empty($collapsedall['gradesonly'])) {
    1846              unset_user_preference('grade_report_grader_collapsed_categories');
    1847          }
    1848  
    1849          return $collapsed;
    1850      }
    1851  
    1852      /**
    1853       * Sets the list of categories that user wants to see collapsed in user preferences
    1854       *
    1855       * This method may filter or even trim the list if it does not fit in DB field.
    1856       *
    1857       * @param int $courseid
    1858       * @param array $collapsed
    1859       */
    1860      protected static function set_collapsed_preferences($courseid, $collapsed) {
    1861          global $DB;
    1862          // In an unlikely case that the list of collapsed categories for one course is too big for the user preference size,
    1863          // try to filter the list of categories since array may contain categories that were deleted.
    1864          if (strlen(json_encode($collapsed)) >= 1333) {
    1865              $collapsed = static::filter_collapsed_categories($courseid, $collapsed);
    1866          }
    1867  
    1868          // If this did not help, "forget" about some of the collapsed categories. Still better than to loose all information.
    1869          while (strlen(json_encode($collapsed)) >= 1333) {
    1870              if (count($collapsed['aggregatesonly'])) {
    1871                  array_pop($collapsed['aggregatesonly']);
    1872              }
    1873              if (count($collapsed['gradesonly'])) {
    1874                  array_pop($collapsed['gradesonly']);
    1875              }
    1876          }
    1877  
    1878          if (!empty($collapsed['aggregatesonly']) || !empty($collapsed['gradesonly'])) {
    1879              set_user_preference('grade_report_grader_collapsed_categories'.$courseid, json_encode($collapsed));
    1880          } else {
    1881              unset_user_preference('grade_report_grader_collapsed_categories'.$courseid);
    1882          }
    1883      }
    1884  
    1885      /**
    1886       * Processes a single action against a category, grade_item or grade.
    1887       * @param string $target eid ({type}{id}, e.g. c4 for category4)
    1888       * @param string $action Which action to take (edit, delete etc...)
    1889       * @param int $courseid affected course.
    1890       * @return
    1891       */
    1892      public static function do_process_action($target, $action, $courseid = null) {
    1893          global $DB;
    1894          // TODO: this code should be in some grade_tree static method
    1895          $targettype = substr($target, 0, 2);
    1896          $targetid = substr($target, 2);
    1897          // TODO: end
    1898  
    1899          if ($targettype !== 'cg') {
    1900              // The following code only works with categories.
    1901              return true;
    1902          }
    1903  
    1904          if (!$courseid) {
    1905              debugging('Function grade_report_grader::do_process_action() now requires additional argument courseid',
    1906                  DEBUG_DEVELOPER);
    1907              if (!$courseid = $DB->get_field('grade_categories', 'courseid', array('id' => $targetid), IGNORE_MISSING)) {
    1908                  return true;
    1909              }
    1910          }
    1911  
    1912          $collapsed = static::get_collapsed_preferences($courseid);
    1913  
    1914          switch ($action) {
    1915              case 'switch_minus': // Add category to array of aggregatesonly
    1916                  if (!in_array($targetid, $collapsed['aggregatesonly'])) {
    1917                      $collapsed['aggregatesonly'][] = $targetid;
    1918                      static::set_collapsed_preferences($courseid, $collapsed);
    1919                  }
    1920                  break;
    1921  
    1922              case 'switch_plus': // Remove category from array of aggregatesonly, and add it to array of gradesonly
    1923                  $key = array_search($targetid, $collapsed['aggregatesonly']);
    1924                  if ($key !== false) {
    1925                      unset($collapsed['aggregatesonly'][$key]);
    1926                  }
    1927                  if (!in_array($targetid, $collapsed['gradesonly'])) {
    1928                      $collapsed['gradesonly'][] = $targetid;
    1929                  }
    1930                  static::set_collapsed_preferences($courseid, $collapsed);
    1931                  break;
    1932              case 'switch_whole': // Remove the category from the array of collapsed cats
    1933                  $key = array_search($targetid, $collapsed['gradesonly']);
    1934                  if ($key !== false) {
    1935                      unset($collapsed['gradesonly'][$key]);
    1936                      static::set_collapsed_preferences($courseid, $collapsed);
    1937                  }
    1938  
    1939                  break;
    1940              default:
    1941                  break;
    1942          }
    1943  
    1944          return true;
    1945      }
    1946  
    1947      /**
    1948       * Refactored function for generating HTML of sorting links with matching arrows.
    1949       * Returns an array with 'studentname' and 'idnumber' as keys, with HTML ready
    1950       * to inject into a table header cell.
    1951       * @param array $extrafields Array of extra fields being displayed, such as
    1952       *   user idnumber
    1953       * @return array An associative array of HTML sorting links+arrows
    1954       */
    1955      public function get_sort_arrows(array $extrafields = array()) {
    1956          global $OUTPUT, $CFG;
    1957          $arrows = array();
    1958  
    1959          $strsortasc   = $this->get_lang_string('sortasc', 'grades');
    1960          $strsortdesc  = $this->get_lang_string('sortdesc', 'grades');
    1961          $iconasc = $OUTPUT->pix_icon('t/sort_asc', $strsortasc, '', array('class' => 'iconsmall sorticon'));
    1962          $icondesc = $OUTPUT->pix_icon('t/sort_desc', $strsortdesc, '', array('class' => 'iconsmall sorticon'));
    1963  
    1964          // Sourced from tablelib.php
    1965          // Check the full name display for sortable fields.
    1966          if (has_capability('moodle/site:viewfullnames', $this->context)) {
    1967              $nameformat = $CFG->alternativefullnameformat;
    1968          } else {
    1969              $nameformat = $CFG->fullnamedisplay;
    1970          }
    1971  
    1972          if ($nameformat == 'language') {
    1973              $nameformat = get_string('fullnamedisplay');
    1974          }
    1975  
    1976          $arrows['studentname'] = '';
    1977          $requirednames = order_in_string(\core_user\fields::get_name_fields(), $nameformat);
    1978          if (!empty($requirednames)) {
    1979              foreach ($requirednames as $name) {
    1980                  $arrows['studentname'] .= html_writer::link(
    1981                      new moodle_url($this->baseurl, array('sortitemid' => $name)), $this->get_lang_string($name)
    1982                  );
    1983                  if ($this->sortitemid == $name) {
    1984                      $arrows['studentname'] .= $this->sortorder == 'ASC' ? $iconasc : $icondesc;
    1985                  }
    1986                  $arrows['studentname'] .= ' / ';
    1987              }
    1988  
    1989              $arrows['studentname'] = substr($arrows['studentname'], 0, -3);
    1990          }
    1991  
    1992          foreach ($extrafields as $field) {
    1993              $fieldlink = html_writer::link(new moodle_url($this->baseurl,
    1994                      array('sortitemid' => $field)), \core_user\fields::get_display_name($field));
    1995              $arrows[$field] = $fieldlink;
    1996  
    1997              if ($field == $this->sortitemid) {
    1998                  if ($this->sortorder == 'ASC') {
    1999                      $arrows[$field] .= $iconasc;
    2000                  } else {
    2001                      $arrows[$field] .= $icondesc;
    2002                  }
    2003              }
    2004          }
    2005  
    2006          return $arrows;
    2007      }
    2008  
    2009      /**
    2010       * Returns the maximum number of students to be displayed on each page
    2011       *
    2012       * @return int The maximum number of students to display per page
    2013       */
    2014      public function get_students_per_page() {
    2015          return $this->get_pref('studentsperpage');
    2016      }
    2017  }
    2018