Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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

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