Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 401 and 402] [Versions 401 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Renderable class for gradehistory report.
  19   *
  20   * @package    gradereport_history
  21   * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  namespace gradereport_history\output;
  26  
  27  defined('MOODLE_INTERNAL') || die;
  28  
  29  require_once($CFG->libdir . '/tablelib.php');
  30  require_once($CFG->dirroot . '/user/lib.php');
  31  
  32  /**
  33   * Renderable class for gradehistory report.
  34   *
  35   * @since      Moodle 2.8
  36   * @package    gradereport_history
  37   * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
  38   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  39   */
  40  class tablelog extends \table_sql implements \renderable {
  41  
  42      /**
  43       * @var int course id.
  44       */
  45      protected $courseid;
  46  
  47      /**
  48       * @var \context context of the page to be rendered.
  49       */
  50      protected $context;
  51  
  52      /**
  53       * @var \stdClass A list of filters to be applied to the sql query.
  54       */
  55      protected $filters;
  56  
  57      /**
  58       * @var \stdClass[] List of users included in the report (if userids are specified as filters)
  59       */
  60      protected $users = [];
  61  
  62      /**
  63       * @var array A list of grade items present in the course.
  64       */
  65      protected $gradeitems = array();
  66  
  67      /**
  68       * @var \course_modinfo|null A list of cm instances in course.
  69       */
  70      protected $cms;
  71  
  72      /**
  73       * @var int The default number of decimal points to use in this course
  74       * when a grade item does not itself define the number of decimal points.
  75       */
  76      protected $defaultdecimalpoints;
  77  
  78      /**
  79       * Sets up the table_log parameters.
  80       *
  81       * @param string $uniqueid unique id of table.
  82       * @param \context_course $context Context of the report.
  83       * @param \moodle_url $url url of the page where this table would be displayed.
  84       * @param array $filters options are:
  85       *                          userids : limit to specific users (default: none)
  86       *                          itemid : limit to specific grade item (default: all)
  87       *                          grader : limit to specific graders (default: all)
  88       *                          datefrom : start of date range
  89       *                          datetill : end of date range
  90       *                          revisedonly : only show revised grades (default: false)
  91       *                          format : page | csv | excel (default: page)
  92       * @param string $download Represents download format, pass '' no download at this time.
  93       * @param int $page The current page being displayed.
  94       * @param int $perpage Number of rules to display per page.
  95       */
  96      public function __construct($uniqueid, \context_course $context, $url, $filters = array(), $download = '', $page = 0,
  97                                  $perpage = 100) {
  98          global $CFG;
  99          parent::__construct($uniqueid);
 100  
 101          $this->set_attribute('class', 'gradereport_history generaltable generalbox');
 102  
 103          // Set protected properties.
 104          $this->context = $context;
 105          $this->courseid = $this->context->instanceid;
 106          $this->pagesize = $perpage;
 107          $this->page = $page;
 108          $this->gradeitems = \grade_item::fetch_all(array('courseid' => $this->courseid));
 109          $this->cms = get_fast_modinfo($this->courseid);
 110          $this->useridfield = 'userid';
 111          $this->defaultdecimalpoints = grade_get_setting($this->courseid, 'decimalpoints', $CFG->grade_decimalpoints);
 112  
 113          // Define columns in the table.
 114          $this->define_table_columns();
 115  
 116          // Define filters.
 117          $this->define_table_filters((object) $filters);
 118  
 119          // Define configs.
 120          $this->define_table_configs($url);
 121  
 122          // Set download status.
 123          $this->is_downloading($download, get_string('exportfilename', 'gradereport_history'));
 124      }
 125  
 126      /**
 127       * Define table configs.
 128       *
 129       * @param \moodle_url $url url of the page where this table would be displayed.
 130       */
 131      protected function define_table_configs(\moodle_url $url) {
 132  
 133          // Set table url.
 134          $urlparams = (array)$this->filters;
 135          unset($urlparams['submitbutton']);
 136          unset($urlparams['userfullnames']);
 137          $url->params($urlparams);
 138          $this->define_baseurl($url);
 139  
 140          // Set table configs.
 141          $this->collapsible(true);
 142          $this->sortable(true, 'timemodified', SORT_DESC);
 143          $this->pageable(true);
 144          $this->no_sorting('grader');
 145      }
 146  
 147      /**
 148       * Define table filters
 149       *
 150       * @param \stdClass $filters
 151       */
 152      protected function define_table_filters(\stdClass $filters): void {
 153          global $DB;
 154  
 155          $this->filters = $filters;
 156  
 157          if (!empty($this->filters->userids)) {
 158  
 159              $course = get_course($this->courseid);
 160  
 161              // Retrieve userids that are part of the filters object, and ensure user can access each of them.
 162              [$userselect, $userparams] = $DB->get_in_or_equal(explode(',', $this->filters->userids), SQL_PARAMS_NAMED);
 163              [$usersort] = users_order_by_sql();
 164  
 165              $this->users = array_filter(
 166                  $DB->get_records_select('user', "id {$userselect}", $userparams, $usersort),
 167                  static function(\stdClass $user) use ($course): bool {
 168                      return user_can_view_profile($user, $course);
 169                  }
 170              );
 171  
 172              // Reset userids to the filtered array of users.
 173              $this->filters->userids = implode(',', array_keys($this->users));
 174          }
 175      }
 176  
 177      /**
 178       * Setup the headers for the html table.
 179       */
 180      protected function define_table_columns() {
 181          $extrafields = \core_user\fields::get_identity_fields($this->context);
 182  
 183          // Define headers and columns.
 184          $cols = array(
 185              'timemodified' => get_string('datetime', 'gradereport_history'),
 186              'fullname' => get_string('name')
 187          );
 188  
 189          // Add headers for extra user fields.
 190          foreach ($extrafields as $field) {
 191              if (get_string_manager()->string_exists($field, 'moodle')) {
 192                  $cols[$field] = get_string($field);
 193              } else {
 194                  $cols[$field] = \core_user\fields::get_display_name($field);
 195              }
 196          }
 197  
 198          // Add remaining headers.
 199          $cols = array_merge($cols, array(
 200              'itemname' => get_string('gradeitem', 'grades'),
 201              'prevgrade' => get_string('gradeold', 'gradereport_history'),
 202              'finalgrade' => get_string('gradenew', 'gradereport_history'),
 203              'grader' => get_string('grader', 'gradereport_history'),
 204              'source' => get_string('source', 'gradereport_history'),
 205              'overridden' => get_string('overridden', 'grades'),
 206              'locked' => get_string('locked', 'grades'),
 207              'excluded' => get_string('excluded', 'gradereport_history'),
 208              'feedback' => get_string('feedbacktext', 'gradereport_history')
 209              )
 210          );
 211  
 212          $this->define_columns(array_keys($cols));
 213          $this->define_headers(array_values($cols));
 214      }
 215  
 216      /**
 217       * Method to display the final grade.
 218       *
 219       * @param \stdClass $history an entry of history record.
 220       *
 221       * @return string HTML to display
 222       */
 223      public function col_finalgrade(\stdClass $history) {
 224          if (!empty($this->gradeitems[$history->itemid])) {
 225              $decimalpoints = $this->gradeitems[$history->itemid]->get_decimals();
 226          } else {
 227              $decimalpoints = $this->defaultdecimalpoints;
 228          }
 229  
 230          return format_float($history->finalgrade, $decimalpoints);
 231      }
 232  
 233      /**
 234       * Method to display the previous grade.
 235       *
 236       * @param \stdClass $history an entry of history record.
 237       *
 238       * @return string HTML to display
 239       */
 240      public function col_prevgrade(\stdClass $history) {
 241          if (!empty($this->gradeitems[$history->itemid])) {
 242              $decimalpoints = $this->gradeitems[$history->itemid]->get_decimals();
 243          } else {
 244              $decimalpoints = $this->defaultdecimalpoints;
 245          }
 246  
 247          return format_float($history->prevgrade, $decimalpoints);
 248      }
 249  
 250      /**
 251       * Method to display column timemodifed.
 252       *
 253       * @param \stdClass $history an entry of history record.
 254       *
 255       * @return string HTML to display
 256       */
 257      public function col_timemodified(\stdClass $history) {
 258          return userdate($history->timemodified);
 259      }
 260  
 261      /**
 262       * Method to display column itemname.
 263       *
 264       * @param \stdClass $history an entry of history record.
 265       *
 266       * @return string HTML to display
 267       */
 268      public function col_itemname(\stdClass $history) {
 269          // Make sure grade item is still present and link it to the module if possible.
 270          $itemid = $history->itemid;
 271          if (!empty($this->gradeitems[$itemid])) {
 272              if ($history->itemtype === 'mod' && !$this->is_downloading()) {
 273                  if (!empty($this->cms->instances[$history->itemmodule][$history->iteminstance])) {
 274                      $cm = $this->cms->instances[$history->itemmodule][$history->iteminstance];
 275                      $url = new \moodle_url('/mod/' . $history->itemmodule . '/view.php', array('id' => $cm->id));
 276                      return \html_writer::link($url, $this->gradeitems[$itemid]->get_name());
 277                  }
 278              }
 279              return $this->gradeitems[$itemid]->get_name();
 280          }
 281          return get_string('deleteditemid', 'gradereport_history', $history->itemid);
 282      }
 283  
 284      /**
 285       * Method to display column grader.
 286       *
 287       * @param \stdClass $history an entry of history record.
 288       *
 289       * @return string HTML to display
 290       */
 291      public function col_grader(\stdClass $history) {
 292          if (empty($history->usermodified)) {
 293              // Not every row has a valid usermodified.
 294              return '';
 295          }
 296  
 297          $grader = new \stdClass();
 298          $grader = username_load_fields_from_object($grader, $history, 'grader');
 299          $name = fullname($grader);
 300  
 301          if ($this->download) {
 302              return $name;
 303          }
 304  
 305          $userid = $history->usermodified;
 306          $profileurl = new \moodle_url('/user/view.php', array('id' => $userid, 'course' => $this->courseid));
 307  
 308          return \html_writer::link($profileurl, $name);
 309      }
 310  
 311      /**
 312       * Method to display column overridden.
 313       *
 314       * @param \stdClass $history an entry of history record.
 315       *
 316       * @return string HTML to display
 317       */
 318      public function col_overridden(\stdClass $history) {
 319          return $history->overridden ? get_string('yes') : get_string('no');
 320      }
 321  
 322      /**
 323       * Method to display column locked.
 324       *
 325       * @param \stdClass $history an entry of history record.
 326       *
 327       * @return string HTML to display
 328       */
 329      public function col_locked(\stdClass $history) {
 330          return $history->locked ? get_string('yes') : get_string('no');
 331      }
 332  
 333      /**
 334       * Method to display column excluded.
 335       *
 336       * @param \stdClass $history an entry of history record.
 337       *
 338       * @return string HTML to display
 339       */
 340      public function col_excluded(\stdClass $history) {
 341          return $history->excluded ? get_string('yes') : get_string('no');
 342      }
 343  
 344      /**
 345       * Method to display column feedback.
 346       *
 347       * @param \stdClass $history an entry of history record.
 348       *
 349       * @return string HTML to display
 350       */
 351      public function col_feedback(\stdClass $history) {
 352          if ($this->is_downloading()) {
 353              return $history->feedback;
 354          } else {
 355              // We need the activity context, not the course context.
 356              $gradeitem = $this->gradeitems[$history->itemid];
 357              $context = $gradeitem->get_context();
 358  
 359              $feedback = file_rewrite_pluginfile_urls(
 360                  $history->feedback,
 361                  'pluginfile.php',
 362                  $context->id,
 363                  GRADE_FILE_COMPONENT,
 364                  GRADE_HISTORY_FEEDBACK_FILEAREA,
 365                  $history->id
 366              );
 367  
 368              return format_text($feedback, $history->feedbackformat, array('context' => $context));
 369          }
 370      }
 371  
 372      /**
 373       * Builds the sql and param list needed, based on the user selected filters.
 374       *
 375       * @return array containing sql to use and an array of params.
 376       */
 377      protected function get_filters_sql_and_params() {
 378          global $DB, $USER;
 379  
 380          $coursecontext = $this->context;
 381          $filter = 'gi.courseid = :courseid';
 382          $params = array(
 383              'courseid' => $coursecontext->instanceid,
 384          );
 385  
 386          if (!empty($this->filters->itemid)) {
 387              $filter .= ' AND ggh.itemid = :itemid';
 388              $params['itemid'] = $this->filters->itemid;
 389          }
 390          if (!empty($this->filters->userids)) {
 391              $list = explode(',', $this->filters->userids);
 392              list($insql, $plist) = $DB->get_in_or_equal($list, SQL_PARAMS_NAMED);
 393              $filter .= " AND ggh.userid $insql";
 394              $params += $plist;
 395          }
 396          if (!empty($this->filters->datefrom)) {
 397              $filter .= " AND ggh.timemodified >= :datefrom";
 398              $params += array('datefrom' => $this->filters->datefrom);
 399          }
 400          if (!empty($this->filters->datetill)) {
 401              $filter .= " AND ggh.timemodified <= :datetill";
 402              $params += array('datetill' => $this->filters->datetill);
 403          }
 404          if (!empty($this->filters->grader)) {
 405              $filter .= " AND ggh.usermodified = :grader";
 406              $params += array('grader' => $this->filters->grader);
 407          }
 408  
 409          // If the course is separate group mode and the current user is not allowed to see all groups make sure
 410          // that we display only users from the same groups as current user.
 411          $groupmode = get_course($coursecontext->instanceid)->groupmode;
 412          if ($groupmode == SEPARATEGROUPS && !has_capability('moodle/site:accessallgroups', $coursecontext)) {
 413              $groupids = array_column(groups_get_all_groups($coursecontext->instanceid, $USER->id, 0, 'g.id'), 'id');
 414              list($gsql, $gparams) = $DB->get_in_or_equal($groupids, SQL_PARAMS_NAMED, 'gmuparam', true, 0);
 415              $filter .= " AND EXISTS (SELECT 1 FROM {groups_members} gmu WHERE gmu.userid=ggh.userid AND gmu.groupid $gsql)";
 416              $params += $gparams;
 417          }
 418  
 419          return array($filter, $params);
 420      }
 421  
 422      /**
 423       * Builds the complete sql with all the joins to get the grade history data.
 424       *
 425       * @param bool $count setting this to true, returns an sql to get count only instead of the complete data records.
 426       *
 427       * @return array containing sql to use and an array of params.
 428       */
 429      protected function get_sql_and_params($count = false) {
 430          $fields = 'ggh.id, ggh.timemodified, ggh.itemid, ggh.userid, ggh.finalgrade, ggh.usermodified,
 431                     ggh.source, ggh.overridden, ggh.locked, ggh.excluded, ggh.feedback, ggh.feedbackformat,
 432                     gi.itemtype, gi.itemmodule, gi.iteminstance, gi.itemnumber, ';
 433  
 434          $userfieldsapi = \core_user\fields::for_identity($this->context);
 435          $userfieldssql = $userfieldsapi->get_sql('u', true, '', '', true);
 436          $userfieldsselects = '';
 437          $userfieldsjoins = '';
 438          $userfieldsparams = [];
 439          if (!$count) {
 440              $userfieldsselects = $userfieldssql->selects;
 441              $userfieldsjoins = $userfieldssql->joins;
 442              $userfieldsparams = $userfieldssql->params;
 443          }
 444  
 445          // Add extra user fields that we need for the graded user.
 446          $extrafields = [];
 447          foreach ($userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]) as $field) {
 448              $extrafields[$field] = $userfieldssql->mappings[$field];
 449          }
 450          $userfieldsapi = \core_user\fields::for_name();
 451          $fields .= $userfieldsapi->get_sql('u', false, '', '', false)->selects . ', ';
 452          $groupby = $fields;
 453  
 454          // Add extra user fields that we need for the grader user.
 455          $fields .= $userfieldsapi->get_sql('ug', false, 'grader', '', false)->selects;
 456          $groupby .= $userfieldsapi->get_sql('ug', false, '', '', false)->selects;
 457  
 458          // Filtering on revised grades only.
 459          $revisedonly = !empty($this->filters->revisedonly);
 460  
 461          if ($count && !$revisedonly) {
 462              // We can only directly use count when not using the filter revised only.
 463              $select = "COUNT(1)";
 464          } else {
 465              // Fetching the previous grade. We use MAX() to ensure that we only get one result if
 466              // more than one histories happened at the same second.
 467              $prevgrade = "SELECT MAX(finalgrade)
 468                              FROM {grade_grades_history} h
 469                             WHERE h.itemid = ggh.itemid
 470                               AND h.userid = ggh.userid
 471                               AND h.timemodified < ggh.timemodified
 472                               AND NOT EXISTS (
 473                                SELECT 1
 474                                  FROM {grade_grades_history} h2
 475                                 WHERE h2.itemid = ggh.itemid
 476                                   AND h2.userid = ggh.userid
 477                                   AND h2.timemodified < ggh.timemodified
 478                                   AND h.timemodified < h2.timemodified)";
 479  
 480              $select = "$fields, ($prevgrade) AS prevgrade,
 481                        CASE WHEN gi.itemname IS NULL THEN gi.itemtype ELSE gi.itemname END AS itemname";
 482          }
 483  
 484          list($where, $params) = $this->get_filters_sql_and_params();
 485  
 486          $sql = " SELECT $select $userfieldsselects
 487                     FROM {grade_grades_history} ggh
 488                     JOIN {grade_items} gi ON gi.id = ggh.itemid
 489                     JOIN {user} u ON u.id = ggh.userid
 490                          $userfieldsjoins
 491                LEFT JOIN {user} ug ON ug.id = ggh.usermodified
 492                    WHERE $where";
 493          $params = array_merge($userfieldsparams, $params);
 494  
 495          // As prevgrade is a dynamic field, we need to wrap the query. This is the only filtering
 496          // that should be defined outside the method self::get_filters_sql_and_params().
 497          if ($revisedonly) {
 498              $allorcount = $count ? 'COUNT(1)' : '*';
 499              $sql = "SELECT $allorcount FROM ($sql) pg
 500                       WHERE pg.finalgrade != pg.prevgrade
 501                          OR (pg.prevgrade IS NULL AND pg.finalgrade IS NOT NULL)
 502                          OR (pg.prevgrade IS NOT NULL AND pg.finalgrade IS NULL)";
 503          }
 504  
 505          // Add order by if needed.
 506          if (!$count && $sqlsort = $this->get_sql_sort()) {
 507              $sql .= " ORDER BY " . $sqlsort;
 508          }
 509  
 510          return array($sql, $params);
 511      }
 512  
 513      /**
 514       * Get the SQL fragment to sort by.
 515       *
 516       * This is overridden to sort by timemodified and ID by default. Many items happen at the same time
 517       * and a second sorting by ID is valuable to distinguish the order in which the history happened.
 518       *
 519       * @return string SQL fragment.
 520       */
 521      public function get_sql_sort() {
 522          $columns = $this->get_sort_columns();
 523          if (count($columns) == 1 && isset($columns['timemodified']) && $columns['timemodified'] == SORT_DESC) {
 524              // Add the 'id' column when we are using the default sorting.
 525              $columns['id'] = SORT_DESC;
 526              return self::construct_order_by($columns);
 527          }
 528          return parent::get_sql_sort();
 529      }
 530  
 531      /**
 532       * Query the reader. Store results in the object for use by build_table.
 533       *
 534       * @param int $pagesize size of page for paginated displayed table.
 535       * @param bool $useinitialsbar do you want to use the initials bar.
 536       */
 537      public function query_db($pagesize, $useinitialsbar = true) {
 538          global $DB;
 539  
 540          list($countsql, $countparams) = $this->get_sql_and_params(true);
 541          list($sql, $params) = $this->get_sql_and_params();
 542          $total = $DB->count_records_sql($countsql, $countparams);
 543          $this->pagesize($pagesize, $total);
 544          if ($this->is_downloading()) {
 545              $histories = $DB->get_records_sql($sql, $params);
 546          } else {
 547              $histories = $DB->get_records_sql($sql, $params, $this->pagesize * $this->page, $this->pagesize);
 548          }
 549          foreach ($histories as $history) {
 550              $this->rawdata[] = $history;
 551          }
 552          // Set initial bars.
 553          if ($useinitialsbar) {
 554              $this->initialbars($total > $pagesize);
 555          }
 556      }
 557  
 558      /**
 559       * Returns a list of selected users.
 560       *
 561       * @return \stdClass[] List of user objects
 562       */
 563      public function get_selected_users(): array {
 564          return $this->users;
 565      }
 566  }