Search moodle.org's
Developer Documentation

See Release Notes

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

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

   1  <?php
   2  
   3  // This file is part of Moodle - http://moodle.org/
   4  //
   5  // Moodle is free software: you can redistribute it and/or modify
   6  // it under the terms of the GNU General Public License as published by
   7  // the Free Software Foundation, either version 3 of the License, or
   8  // (at your option) any later version.
   9  //
  10  // Moodle is distributed in the hope that it will be useful,
  11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  13  // GNU General Public License for more details.
  14  //
  15  // You should have received a copy of the GNU General Public License
  16  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  17  
  18  /**
  19   * @package    core
  20   * @subpackage lib
  21   * @copyright  1999 onwards Martin Dougiamas  {@link http://moodle.com}
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  /**#@+
  29   * These constants relate to the table's handling of URL parameters.
  30   */
  31  define('TABLE_VAR_SORT',   1);
  32  define('TABLE_VAR_HIDE',   2);
  33  define('TABLE_VAR_SHOW',   3);
  34  define('TABLE_VAR_IFIRST', 4);
  35  define('TABLE_VAR_ILAST',  5);
  36  define('TABLE_VAR_PAGE',   6);
  37  define('TABLE_VAR_RESET',  7);
  38  define('TABLE_VAR_DIR',    8);
  39  /**#@-*/
  40  
  41  /**#@+
  42   * Constants that indicate whether the paging bar for the table
  43   * appears above or below the table.
  44   */
  45  define('TABLE_P_TOP',    1);
  46  define('TABLE_P_BOTTOM', 2);
  47  /**#@-*/
  48  
  49  /**
  50   * Constant that defines the 'Show all' page size.
  51   */
  52  define('TABLE_SHOW_ALL_PAGE_SIZE', 5000);
  53  
  54  use core_table\local\filter\filterset;
  55  
  56  /**
  57   * @package   moodlecore
  58   * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
  59   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  60   */
  61  class flexible_table {
  62  
  63      var $uniqueid        = NULL;
  64      var $attributes      = array();
  65      var $headers         = array();
  66  
  67      /**
  68       * @var string A column which should be considered as a header column.
  69       */
  70      protected $headercolumn = null;
  71  
  72      /**
  73       * @var string For create header with help icon.
  74       */
  75      private $helpforheaders = array();
  76      var $columns         = array();
  77      var $column_style    = array();
  78      var $column_class    = array();
  79      var $column_suppress = array();
  80      var $column_nosort   = array('userpic');
  81      private $column_textsort = array();
  82      /** @var boolean Stores if setup has already been called on this flixible table. */
  83      var $setup           = false;
  84      var $baseurl         = NULL;
  85      var $request         = array();
  86  
  87      /** @var string[] Columns that are expected to contain a users fullname.  */
  88      protected $userfullnamecolumns = ['fullname'];
  89  
  90      /** @var array[] Attributes for each column  */
  91      private $columnsattributes = [];
  92  
  93      /**
  94       * @var bool Whether or not to store table properties in the user_preferences table.
  95       */
  96      private $persistent = false;
  97      var $is_collapsible = false;
  98      var $is_sortable    = false;
  99  
 100      /**
 101       * @var array The fields to sort.
 102       */
 103      protected $sortdata;
 104  
 105      /** @var string The manually set first name initial preference */
 106      protected $ifirst;
 107  
 108      /** @var string The manually set last name initial preference */
 109      protected $ilast;
 110  
 111      var $use_pages      = false;
 112      var $use_initials   = false;
 113  
 114      var $maxsortkeys = 2;
 115      var $pagesize    = 30;
 116      var $currpage    = 0;
 117      var $totalrows   = 0;
 118      var $currentrow  = 0;
 119      var $sort_default_column = NULL;
 120      var $sort_default_order  = SORT_ASC;
 121  
 122      /** @var integer The defeult per page size for the table. */
 123      private $defaultperpage = 30;
 124  
 125      /**
 126       * Array of positions in which to display download controls.
 127       */
 128      var $showdownloadbuttonsat= array(TABLE_P_TOP);
 129  
 130      /**
 131       * @var string Key of field returned by db query that is the id field of the
 132       * user table or equivalent.
 133       */
 134      public $useridfield = 'id';
 135  
 136      /**
 137       * @var string which download plugin to use. Default '' means none - print
 138       * html table with paging. Property set by is_downloading which typically
 139       * passes in cleaned data from $
 140       */
 141      var $download  = '';
 142  
 143      /**
 144       * @var bool whether data is downloadable from table. Determines whether
 145       * to display download buttons. Set by method downloadable().
 146       */
 147      var $downloadable = false;
 148  
 149      /**
 150       * @var bool Has start output been called yet?
 151       */
 152      var $started_output = false;
 153  
 154      /** @var table_dataformat_export_format */
 155      var $exportclass = null;
 156  
 157      /**
 158       * @var array For storing user-customised table properties in the user_preferences db table.
 159       */
 160      private $prefs = array();
 161  
 162      /** @var string $sheettitle */
 163      protected $sheettitle;
 164  
 165      /** @var string $filename */
 166      protected $filename;
 167  
 168      /** @var array $hiddencolumns List of hidden columns. */
 169      protected $hiddencolumns;
 170  
 171      /** @var bool $resetting Whether the table preferences is resetting. */
 172      protected $resetting;
 173  
 174      /**
 175       * @var string $caption The caption of table
 176       */
 177      public $caption;
 178  
 179      /**
 180       * @var array $captionattributes The caption attributes of table
 181       */
 182      public $captionattributes;
 183  
 184      /**
 185       * @var filterset The currently applied filerset
 186       * This is required for dynamic tables, but can be used by other tables too if desired.
 187       */
 188      protected $filterset = null;
 189  
 190      /**
 191       * Constructor
 192       * @param string $uniqueid all tables have to have a unique id, this is used
 193       *      as a key when storing table properties like sort order in the session.
 194       */
 195      function __construct($uniqueid) {
 196          $this->uniqueid = $uniqueid;
 197          $this->request  = array(
 198              TABLE_VAR_SORT   => 'tsort',
 199              TABLE_VAR_HIDE   => 'thide',
 200              TABLE_VAR_SHOW   => 'tshow',
 201              TABLE_VAR_IFIRST => 'tifirst',
 202              TABLE_VAR_ILAST  => 'tilast',
 203              TABLE_VAR_PAGE   => 'page',
 204              TABLE_VAR_RESET  => 'treset',
 205              TABLE_VAR_DIR    => 'tdir',
 206          );
 207      }
 208  
 209      /**
 210       * Call this to pass the download type. Use :
 211       *         $download = optional_param('download', '', PARAM_ALPHA);
 212       * To get the download type. We assume that if you call this function with
 213       * params that this table's data is downloadable, so we call is_downloadable
 214       * for you (even if the param is '', which means no download this time.
 215       * Also you can call this method with no params to get the current set
 216       * download type.
 217       * @param string $download dataformat type. One of csv, xhtml, ods, etc
 218       * @param string $filename filename for downloads without file extension.
 219       * @param string $sheettitle title for downloaded data.
 220       * @return string download dataformat type. One of csv, xhtml, ods, etc
 221       */
 222      function is_downloading($download = null, $filename='', $sheettitle='') {
 223          if ($download!==null) {
 224              $this->sheettitle = $sheettitle;
 225              $this->is_downloadable(true);
 226              $this->download = $download;
 227              $this->filename = clean_filename($filename);
 228              $this->export_class_instance();
 229          }
 230          return $this->download;
 231      }
 232  
 233      /**
 234       * Get, and optionally set, the export class.
 235       * @param table_dataformat_export_format $exportclass (optional) if passed, set the table to use this export class.
 236       * @return table_dataformat_export_format the export class in use (after any set).
 237       */
 238      function export_class_instance($exportclass = null) {
 239          if (!is_null($exportclass)) {
 240              $this->started_output = true;
 241              $this->exportclass = $exportclass;
 242              $this->exportclass->table = $this;
 243          } else if (is_null($this->exportclass) && !empty($this->download)) {
 244              $this->exportclass = new table_dataformat_export_format($this, $this->download);
 245              if (!$this->exportclass->document_started()) {
 246                  $this->exportclass->start_document($this->filename, $this->sheettitle);
 247              }
 248          }
 249          return $this->exportclass;
 250      }
 251  
 252      /**
 253       * Probably don't need to call this directly. Calling is_downloading with a
 254       * param automatically sets table as downloadable.
 255       *
 256       * @param bool $downloadable optional param to set whether data from
 257       * table is downloadable. If ommitted this function can be used to get
 258       * current state of table.
 259       * @return bool whether table data is set to be downloadable.
 260       */
 261      function is_downloadable($downloadable = null) {
 262          if ($downloadable !== null) {
 263              $this->downloadable = $downloadable;
 264          }
 265          return $this->downloadable;
 266      }
 267  
 268      /**
 269       * Call with boolean true to store table layout changes in the user_preferences table.
 270       * Note: user_preferences.value has a maximum length of 1333 characters.
 271       * Call with no parameter to get current state of table persistence.
 272       *
 273       * @param bool $persistent Optional parameter to set table layout persistence.
 274       * @return bool Whether or not the table layout preferences will persist.
 275       */
 276      public function is_persistent($persistent = null) {
 277          if ($persistent == true) {
 278              $this->persistent = true;
 279          }
 280          return $this->persistent;
 281      }
 282  
 283      /**
 284       * Where to show download buttons.
 285       * @param array $showat array of postions in which to show download buttons.
 286       * Containing TABLE_P_TOP and/or TABLE_P_BOTTOM
 287       */
 288      function show_download_buttons_at($showat) {
 289          $this->showdownloadbuttonsat = $showat;
 290      }
 291  
 292      /**
 293       * Sets the is_sortable variable to the given boolean, sort_default_column to
 294       * the given string, and the sort_default_order to the given integer.
 295       * @param bool $bool
 296       * @param string $defaultcolumn
 297       * @param int $defaultorder
 298       * @return void
 299       */
 300      function sortable($bool, $defaultcolumn = NULL, $defaultorder = SORT_ASC) {
 301          $this->is_sortable = $bool;
 302          $this->sort_default_column = $defaultcolumn;
 303          $this->sort_default_order  = $defaultorder;
 304      }
 305  
 306      /**
 307       * Use text sorting functions for this column (required for text columns with Oracle).
 308       * Be warned that you cannot use this with column aliases. You can only do this
 309       * with real columns. See MDL-40481 for an example.
 310       * @param string column name
 311       */
 312      function text_sorting($column) {
 313          $this->column_textsort[] = $column;
 314      }
 315  
 316      /**
 317       * Do not sort using this column
 318       * @param string column name
 319       */
 320      function no_sorting($column) {
 321          $this->column_nosort[] = $column;
 322      }
 323  
 324      /**
 325       * Is the column sortable?
 326       * @param string column name, null means table
 327       * @return bool
 328       */
 329      function is_sortable($column = null) {
 330          if (empty($column)) {
 331              return $this->is_sortable;
 332          }
 333          if (!$this->is_sortable) {
 334              return false;
 335          }
 336          return !in_array($column, $this->column_nosort);
 337      }
 338  
 339      /**
 340       * Sets the is_collapsible variable to the given boolean.
 341       * @param bool $bool
 342       * @return void
 343       */
 344      function collapsible($bool) {
 345          $this->is_collapsible = $bool;
 346      }
 347  
 348      /**
 349       * Sets the use_pages variable to the given boolean.
 350       * @param bool $bool
 351       * @return void
 352       */
 353      function pageable($bool) {
 354          $this->use_pages = $bool;
 355      }
 356  
 357      /**
 358       * Sets the use_initials variable to the given boolean.
 359       * @param bool $bool
 360       * @return void
 361       */
 362      function initialbars($bool) {
 363          $this->use_initials = $bool;
 364      }
 365  
 366      /**
 367       * Sets the pagesize variable to the given integer, the totalrows variable
 368       * to the given integer, and the use_pages variable to true.
 369       * @param int $perpage
 370       * @param int $total
 371       * @return void
 372       */
 373      function pagesize($perpage, $total) {
 374          $this->pagesize  = $perpage;
 375          $this->totalrows = $total;
 376          $this->use_pages = true;
 377      }
 378  
 379      /**
 380       * Assigns each given variable in the array to the corresponding index
 381       * in the request class variable.
 382       * @param array $variables
 383       * @return void
 384       */
 385      function set_control_variables($variables) {
 386          foreach ($variables as $what => $variable) {
 387              if (isset($this->request[$what])) {
 388                  $this->request[$what] = $variable;
 389              }
 390          }
 391      }
 392  
 393      /**
 394       * Gives the given $value to the $attribute index of $this->attributes.
 395       * @param string $attribute
 396       * @param mixed $value
 397       * @return void
 398       */
 399      function set_attribute($attribute, $value) {
 400          $this->attributes[$attribute] = $value;
 401      }
 402  
 403      /**
 404       * What this method does is set the column so that if the same data appears in
 405       * consecutive rows, then it is not repeated.
 406       *
 407       * For example, in the quiz overview report, the fullname column is set to be suppressed, so
 408       * that when one student has made multiple attempts, their name is only printed in the row
 409       * for their first attempt.
 410       * @param int $column the index of a column.
 411       */
 412      function column_suppress($column) {
 413          if (isset($this->column_suppress[$column])) {
 414              $this->column_suppress[$column] = true;
 415          }
 416      }
 417  
 418      /**
 419       * Sets the given $column index to the given $classname in $this->column_class.
 420       * @param int $column
 421       * @param string $classname
 422       * @return void
 423       */
 424      function column_class($column, $classname) {
 425          if (isset($this->column_class[$column])) {
 426              $this->column_class[$column] = ' '.$classname; // This space needed so that classnames don't run together in the HTML
 427          }
 428      }
 429  
 430      /**
 431       * Sets the given $column index and $property index to the given $value in $this->column_style.
 432       * @param int $column
 433       * @param string $property
 434       * @param mixed $value
 435       * @return void
 436       */
 437      function column_style($column, $property, $value) {
 438          if (isset($this->column_style[$column])) {
 439              $this->column_style[$column][$property] = $value;
 440          }
 441      }
 442  
 443      /**
 444       * Sets the given $attributes to $this->columnsattributes.
 445       * Column attributes will be added to every cell in the column.
 446       *
 447       * @param array[] $attributes e.g. ['c0_firstname' => ['data-foo' => 'bar']]
 448       */
 449      public function set_columnsattributes(array $attributes): void {
 450          $this->columnsattributes = $attributes;
 451      }
 452  
 453      /**
 454       * Sets all columns' $propertys to the given $value in $this->column_style.
 455       * @param int $property
 456       * @param string $value
 457       * @return void
 458       */
 459      function column_style_all($property, $value) {
 460          foreach (array_keys($this->columns) as $column) {
 461              $this->column_style[$column][$property] = $value;
 462          }
 463      }
 464  
 465      /**
 466       * Sets $this->baseurl.
 467       * @param moodle_url|string $url the url with params needed to call up this page
 468       */
 469      function define_baseurl($url) {
 470          $this->baseurl = new moodle_url($url);
 471      }
 472  
 473      /**
 474       * @param array $columns an array of identifying names for columns. If
 475       * columns are sorted then column names must correspond to a field in sql.
 476       */
 477      function define_columns($columns) {
 478          $this->columns = array();
 479          $this->column_style = array();
 480          $this->column_class = array();
 481          $this->columnsattributes = [];
 482          $colnum = 0;
 483  
 484          foreach ($columns as $column) {
 485              $this->columns[$column]         = $colnum++;
 486              $this->column_style[$column]    = array();
 487              $this->column_class[$column]    = '';
 488              $this->columnsattributes[$column] = [];
 489              $this->column_suppress[$column] = false;
 490          }
 491      }
 492  
 493      /**
 494       * @param array $headers numerical keyed array of displayed string titles
 495       * for each column.
 496       */
 497      function define_headers($headers) {
 498          $this->headers = $headers;
 499      }
 500  
 501      /**
 502       * Mark a specific column as being a table header using the column name defined in define_columns.
 503       *
 504       * Note: Only one column can be a header, and it will be rendered using a th tag.
 505       *
 506       * @param   string  $column
 507       */
 508      public function define_header_column(string $column) {
 509          $this->headercolumn = $column;
 510      }
 511  
 512      /**
 513       * Defines a help icon for the header
 514       *
 515       * Always use this function if you need to create header with sorting and help icon.
 516       *
 517       * @param renderable[] $helpicons An array of renderable objects to be used as help icons
 518       */
 519      public function define_help_for_headers($helpicons) {
 520          $this->helpforheaders = $helpicons;
 521      }
 522  
 523      /**
 524       * Mark the table preferences to be reset.
 525       */
 526      public function mark_table_to_reset(): void {
 527          $this->resetting = true;
 528      }
 529  
 530      /**
 531       * Is the table marked for reset preferences?
 532       *
 533       * @return bool True if the table is marked to reset, false otherwise.
 534       */
 535      protected function is_resetting_preferences(): bool {
 536          if ($this->resetting === null) {
 537              $this->resetting = optional_param($this->request[TABLE_VAR_RESET], false, PARAM_BOOL);
 538          }
 539  
 540          return $this->resetting;
 541  }
 542  
 543      /**
 544       * Must be called after table is defined. Use methods above first. Cannot
 545       * use functions below till after calling this method.
 546       */
 547      function setup() {
 548  
 549          if (empty($this->columns) || empty($this->uniqueid)) {
 550              return false;
 551          }
 552  
 553          $this->initialise_table_preferences();
 554  
 555          if (empty($this->baseurl)) {
 556              debugging('You should set baseurl when using flexible_table.');
 557              global $PAGE;
 558              $this->baseurl = $PAGE->url;
 559          }
 560  
 561          if ($this->currpage == null) {
 562              $this->currpage = optional_param($this->request[TABLE_VAR_PAGE], 0, PARAM_INT);
 563          }
 564  
 565          $this->setup = true;
 566  
 567          // Always introduce the "flexible" class for the table if not specified
 568          if (empty($this->attributes)) {
 569              $this->attributes['class'] = 'flexible table table-striped table-hover';
 570          } else if (!isset($this->attributes['class'])) {
 571              $this->attributes['class'] = 'flexible table table-striped table-hover';
 572          } else if (!in_array('flexible', explode(' ', $this->attributes['class']))) {
 573              $this->attributes['class'] = trim('flexible table table-striped table-hover ' . $this->attributes['class']);
 574          }
 575      }
 576  
 577      /**
 578       * Get the order by clause from the session or user preferences, for the table with id $uniqueid.
 579       * @param string $uniqueid the identifier for a table.
 580       * @return string SQL fragment that can be used in an ORDER BY clause.
 581       */
 582      public static function get_sort_for_table($uniqueid) {
 583          global $SESSION;
 584          if (isset($SESSION->flextable[$uniqueid])) {
 585              $prefs = $SESSION->flextable[$uniqueid];
 586          } else if (!$prefs = json_decode(get_user_preferences("flextable_{$uniqueid}", ''), true)) {
 587              return '';
 588          }
 589  
 590          if (empty($prefs['sortby'])) {
 591              return '';
 592          }
 593          if (empty($prefs['textsort'])) {
 594              $prefs['textsort'] = array();
 595          }
 596  
 597          return self::construct_order_by($prefs['sortby'], $prefs['textsort']);
 598      }
 599  
 600      /**
 601       * Prepare an an order by clause from the list of columns to be sorted.
 602       * @param array $cols column name => SORT_ASC or SORT_DESC
 603       * @return string SQL fragment that can be used in an ORDER BY clause.
 604       */
 605      public static function construct_order_by($cols, $textsortcols=array()) {
 606          global $DB;
 607          $bits = array();
 608  
 609          foreach ($cols as $column => $order) {
 610              if (in_array($column, $textsortcols)) {
 611                  $column = $DB->sql_order_by_text($column);
 612              }
 613              if ($order == SORT_ASC) {
 614                  $bits[] = $DB->sql_order_by_null($column);
 615              } else {
 616                  $bits[] = $DB->sql_order_by_null($column, SORT_DESC);
 617              }
 618          }
 619  
 620          return implode(', ', $bits);
 621      }
 622  
 623      /**
 624       * @return string SQL fragment that can be used in an ORDER BY clause.
 625       */
 626      public function get_sql_sort() {
 627          return self::construct_order_by($this->get_sort_columns(), $this->column_textsort);
 628      }
 629  
 630      /**
 631       * Whether the current table contains any fullname columns
 632       *
 633       * @return bool
 634       */
 635      private function contains_fullname_columns(): bool {
 636          $fullnamecolumns = array_intersect_key($this->columns, array_flip($this->userfullnamecolumns));
 637  
 638          return !empty($fullnamecolumns);
 639      }
 640  
 641      /**
 642       * Get the columns to sort by, in the form required by {@link construct_order_by()}.
 643       * @return array column name => SORT_... constant.
 644       */
 645      public function get_sort_columns() {
 646          if (!$this->setup) {
 647              throw new coding_exception('Cannot call get_sort_columns until you have called setup.');
 648          }
 649  
 650          if (empty($this->prefs['sortby'])) {
 651              return array();
 652          }
 653          foreach ($this->prefs['sortby'] as $column => $notused) {
 654              if (isset($this->columns[$column])) {
 655                  continue; // This column is OK.
 656              }
 657              if (in_array($column, \core_user\fields::get_name_fields()) && $this->contains_fullname_columns()) {
 658                  continue; // This column is OK.
 659              }
 660              // This column is not OK.
 661              unset($this->prefs['sortby'][$column]);
 662          }
 663  
 664          return $this->prefs['sortby'];
 665      }
 666  
 667      /**
 668       * @return int the offset for LIMIT clause of SQL
 669       */
 670      function get_page_start() {
 671          if (!$this->use_pages) {
 672              return '';
 673          }
 674          return $this->currpage * $this->pagesize;
 675      }
 676  
 677      /**
 678       * @return int the pagesize for LIMIT clause of SQL
 679       */
 680      function get_page_size() {
 681          if (!$this->use_pages) {
 682              return '';
 683          }
 684          return $this->pagesize;
 685      }
 686  
 687      /**
 688       * @return string sql to add to where statement.
 689       */
 690      function get_sql_where() {
 691          global $DB;
 692  
 693          $conditions = array();
 694          $params = array();
 695  
 696          if ($this->contains_fullname_columns()) {
 697              static $i = 0;
 698              $i++;
 699  
 700              if (!empty($this->prefs['i_first'])) {
 701                  $conditions[] = $DB->sql_like('firstname', ':ifirstc'.$i, false, false);
 702                  $params['ifirstc'.$i] = $this->prefs['i_first'].'%';
 703              }
 704              if (!empty($this->prefs['i_last'])) {
 705                  $conditions[] = $DB->sql_like('lastname', ':ilastc'.$i, false, false);
 706                  $params['ilastc'.$i] = $this->prefs['i_last'].'%';
 707              }
 708          }
 709  
 710          return array(implode(" AND ", $conditions), $params);
 711      }
 712  
 713      /**
 714       * Add a row of data to the table. This function takes an array or object with
 715       * column names as keys or property names.
 716       *
 717       * It ignores any elements with keys that are not defined as columns. It
 718       * puts in empty strings into the row when there is no element in the passed
 719       * array corresponding to a column in the table. It puts the row elements in
 720       * the proper order (internally row table data is stored by in arrays with
 721       * a numerical index corresponding to the column number).
 722       *
 723       * @param object|array $rowwithkeys array keys or object property names are column names,
 724       *                                      as defined in call to define_columns.
 725       * @param string $classname CSS class name to add to this row's tr tag.
 726       */
 727      function add_data_keyed($rowwithkeys, $classname = '') {
 728          $this->add_data($this->get_row_from_keyed($rowwithkeys), $classname);
 729      }
 730  
 731      /**
 732       * Add a number of rows to the table at once. And optionally finish output after they have been added.
 733       *
 734       * @param (object|array|null)[] $rowstoadd Array of rows to add to table, a null value in array adds a separator row. Or a
 735       *                                  object or array is added to table. We expect properties for the row array as would be
 736       *                                  passed to add_data_keyed.
 737       * @param bool     $finish
 738       */
 739      public function format_and_add_array_of_rows($rowstoadd, $finish = true) {
 740          foreach ($rowstoadd as $row) {
 741              if (is_null($row)) {
 742                  $this->add_separator();
 743              } else {
 744                  $this->add_data_keyed($this->format_row($row));
 745              }
 746          }
 747          if ($finish) {
 748              $this->finish_output(!$this->is_downloading());
 749          }
 750      }
 751  
 752      /**
 753       * Add a seperator line to table.
 754       */
 755      function add_separator() {
 756          if (!$this->setup) {
 757              return false;
 758          }
 759          $this->add_data(NULL);
 760      }
 761  
 762      /**
 763       * This method actually directly echoes the row passed to it now or adds it
 764       * to the download. If this is the first row and start_output has not
 765       * already been called this method also calls start_output to open the table
 766       * or send headers for the downloaded.
 767       * Can be used as before. print_html now calls finish_html to close table.
 768       *
 769       * @param array $row a numerically keyed row of data to add to the table.
 770       * @param string $classname CSS class name to add to this row's tr tag.
 771       * @return bool success.
 772       */
 773      function add_data($row, $classname = '') {
 774          if (!$this->setup) {
 775              return false;
 776          }
 777          if (!$this->started_output) {
 778              $this->start_output();
 779          }
 780          if ($this->exportclass!==null) {
 781              if ($row === null) {
 782                  $this->exportclass->add_seperator();
 783              } else {
 784                  $this->exportclass->add_data($row);
 785              }
 786          } else {
 787              $this->print_row($row, $classname);
 788          }
 789          return true;
 790      }
 791  
 792      /**
 793       * You should call this to finish outputting the table data after adding
 794       * data to the table with add_data or add_data_keyed.
 795       *
 796       */
 797      function finish_output($closeexportclassdoc = true) {
 798          if ($this->exportclass!==null) {
 799              $this->exportclass->finish_table();
 800              if ($closeexportclassdoc) {
 801                  $this->exportclass->finish_document();
 802              }
 803          } else {
 804              $this->finish_html();
 805          }
 806      }
 807  
 808      /**
 809       * Hook that can be overridden in child classes to wrap a table in a form
 810       * for example. Called only when there is data to display and not
 811       * downloading.
 812       */
 813      function wrap_html_start() {
 814      }
 815  
 816      /**
 817       * Hook that can be overridden in child classes to wrap a table in a form
 818       * for example. Called only when there is data to display and not
 819       * downloading.
 820       */
 821      function wrap_html_finish() {
 822      }
 823  
 824      /**
 825       * Call appropriate methods on this table class to perform any processing on values before displaying in table.
 826       * Takes raw data from the database and process it into human readable format, perhaps also adding html linking when
 827       * displaying table as html, adding a div wrap, etc.
 828       *
 829       * See for example col_fullname below which will be called for a column whose name is 'fullname'.
 830       *
 831       * @param array|object $row row of data from db used to make one row of the table.
 832       * @return array one row for the table, added using add_data_keyed method.
 833       */
 834      function format_row($row) {
 835          if (is_array($row)) {
 836              $row = (object)$row;
 837          }
 838          $formattedrow = array();
 839          foreach (array_keys($this->columns) as $column) {
 840              $colmethodname = 'col_'.$column;
 841              if (method_exists($this, $colmethodname)) {
 842                  $formattedcolumn = $this->$colmethodname($row);
 843              } else {
 844                  $formattedcolumn = $this->other_cols($column, $row);
 845                  if ($formattedcolumn===NULL) {
 846                      $formattedcolumn = $row->$column;
 847                  }
 848              }
 849              $formattedrow[$column] = $formattedcolumn;
 850          }
 851          return $formattedrow;
 852      }
 853  
 854      /**
 855       * Fullname is treated as a special columname in tablelib and should always
 856       * be treated the same as the fullname of a user.
 857       * @uses $this->useridfield if the userid field is not expected to be id
 858       * then you need to override $this->useridfield to point at the correct
 859       * field for the user id.
 860       *
 861       * @param object $row the data from the db containing all fields from the
 862       *                    users table necessary to construct the full name of the user in
 863       *                    current language.
 864       * @return string contents of cell in column 'fullname', for this row.
 865       */
 866      function col_fullname($row) {
 867          global $COURSE;
 868  
 869          $name = fullname($row, has_capability('moodle/site:viewfullnames', $this->get_context()));
 870          if ($this->download) {
 871              return $name;
 872          }
 873  
 874          $userid = $row->{$this->useridfield};
 875          if ($COURSE->id == SITEID) {
 876              $profileurl = new moodle_url('/user/profile.php', array('id' => $userid));
 877          } else {
 878              $profileurl = new moodle_url('/user/view.php',
 879                      array('id' => $userid, 'course' => $COURSE->id));
 880          }
 881          return html_writer::link($profileurl, $name);
 882      }
 883  
 884      /**
 885       * You can override this method in a child class. See the description of
 886       * build_table which calls this method.
 887       */
 888      function other_cols($column, $row) {
 889          if (isset($row->$column) && ($column === 'email' || $column === 'idnumber') &&
 890                  (!$this->is_downloading() || $this->export_class_instance()->supports_html())) {
 891              // Columns email and idnumber may potentially contain malicious characters, escape them by default.
 892              // This function will not be executed if the child class implements col_email() or col_idnumber().
 893              return s($row->$column);
 894          }
 895          return NULL;
 896      }
 897  
 898      /**
 899       * Used from col_* functions when text is to be displayed. Does the
 900       * right thing - either converts text to html or strips any html tags
 901       * depending on if we are downloading and what is the download type. Params
 902       * are the same as format_text function in weblib.php but some default
 903       * options are changed.
 904       */
 905      function format_text($text, $format=FORMAT_MOODLE, $options=NULL, $courseid=NULL) {
 906          if (!$this->is_downloading()) {
 907              if (is_null($options)) {
 908                  $options = new stdClass;
 909              }
 910              //some sensible defaults
 911              if (!isset($options->para)) {
 912                  $options->para = false;
 913              }
 914              if (!isset($options->newlines)) {
 915                  $options->newlines = false;
 916              }
 917              if (!isset($options->smiley)) {
 918                  $options->smiley = false;
 919              }
 920              if (!isset($options->filter)) {
 921                  $options->filter = false;
 922              }
 923              return format_text($text, $format, $options);
 924          } else {
 925              $eci = $this->export_class_instance();
 926              return $eci->format_text($text, $format, $options, $courseid);
 927          }
 928      }
 929      /**
 930       * This method is deprecated although the old api is still supported.
 931       * @deprecated 1.9.2 - Jun 2, 2008
 932       */
 933      function print_html() {
 934          if (!$this->setup) {
 935              return false;
 936          }
 937          $this->finish_html();
 938      }
 939  
 940      /**
 941       * This function is not part of the public api.
 942       * @return string initial of first name we are currently filtering by
 943       */
 944      function get_initial_first() {
 945          if (!$this->use_initials) {
 946              return NULL;
 947          }
 948  
 949          return $this->prefs['i_first'];
 950      }
 951  
 952      /**
 953       * This function is not part of the public api.
 954       * @return string initial of last name we are currently filtering by
 955       */
 956      function get_initial_last() {
 957          if (!$this->use_initials) {
 958              return NULL;
 959          }
 960  
 961          return $this->prefs['i_last'];
 962      }
 963  
 964      /**
 965       * Helper function, used by {@link print_initials_bar()} to output one initial bar.
 966       * @param array $alpha of letters in the alphabet.
 967       * @param string $current the currently selected letter.
 968       * @param string $class class name to add to this initial bar.
 969       * @param string $title the name to put in front of this initial bar.
 970       * @param string $urlvar URL parameter name for this initial.
 971       *
 972       * @deprecated since Moodle 3.3
 973       */
 974      protected function print_one_initials_bar($alpha, $current, $class, $title, $urlvar) {
 975  
 976          debugging('Method print_one_initials_bar() is no longer used and has been deprecated, ' .
 977              'to print initials bar call print_initials_bar()', DEBUG_DEVELOPER);
 978  
 979          echo html_writer::start_tag('div', array('class' => 'initialbar ' . $class)) .
 980              $title . ' : ';
 981          if ($current) {
 982              echo html_writer::link($this->baseurl->out(false, array($urlvar => '')), get_string('all'));
 983          } else {
 984              echo html_writer::tag('strong', get_string('all'));
 985          }
 986  
 987          foreach ($alpha as $letter) {
 988              if ($letter === $current) {
 989                  echo html_writer::tag('strong', $letter);
 990              } else {
 991                  echo html_writer::link($this->baseurl->out(false, array($urlvar => $letter)), $letter);
 992              }
 993          }
 994  
 995          echo html_writer::end_tag('div');
 996      }
 997  
 998      /**
 999       * This function is not part of the public api.
1000       */
1001      function print_initials_bar() {
1002          global $OUTPUT;
1003  
1004          $ifirst = $this->get_initial_first();
1005          $ilast = $this->get_initial_last();
1006          if (is_null($ifirst)) {
1007              $ifirst = '';
1008          }
1009          if (is_null($ilast)) {
1010              $ilast = '';
1011          }
1012  
1013          if ((!empty($ifirst) || !empty($ilast) || $this->use_initials) && $this->contains_fullname_columns()) {
1014              $prefixfirst = $this->request[TABLE_VAR_IFIRST];
1015              $prefixlast = $this->request[TABLE_VAR_ILAST];
1016              echo $OUTPUT->initials_bar($ifirst, 'firstinitial', get_string('firstname'), $prefixfirst, $this->baseurl);
1017              echo $OUTPUT->initials_bar($ilast, 'lastinitial', get_string('lastname'), $prefixlast, $this->baseurl);
1018          }
1019  
1020      }
1021  
1022      /**
1023       * This function is not part of the public api.
1024       */
1025      function print_nothing_to_display() {
1026          global $OUTPUT;
1027  
1028          // Render the dynamic table header.
1029          echo $this->get_dynamic_table_html_start();
1030  
1031          // Render button to allow user to reset table preferences.
1032          echo $this->render_reset_button();
1033  
1034          $this->print_initials_bar();
1035  
1036          echo $OUTPUT->heading(get_string('nothingtodisplay'));
1037  
1038          // Render the dynamic table footer.
1039          echo $this->get_dynamic_table_html_end();
1040      }
1041  
1042      /**
1043       * This function is not part of the public api.
1044       */
1045      function get_row_from_keyed($rowwithkeys) {
1046          if (is_object($rowwithkeys)) {
1047              $rowwithkeys = (array)$rowwithkeys;
1048          }
1049          $row = array();
1050          foreach (array_keys($this->columns) as $column) {
1051              if (isset($rowwithkeys[$column])) {
1052                  $row [] = $rowwithkeys[$column];
1053              } else {
1054                  $row[] ='';
1055              }
1056          }
1057          return $row;
1058      }
1059  
1060      /**
1061       * Get the html for the download buttons
1062       *
1063       * Usually only use internally
1064       */
1065      public function download_buttons() {
1066          global $OUTPUT;
1067  
1068          if ($this->is_downloadable() && !$this->is_downloading()) {
1069              return $OUTPUT->download_dataformat_selector(get_string('downloadas', 'table'),
1070                      $this->baseurl->out_omit_querystring(), 'download', $this->baseurl->params());
1071          } else {
1072              return '';
1073          }
1074      }
1075  
1076      /**
1077       * This function is not part of the public api.
1078       * You don't normally need to call this. It is called automatically when
1079       * needed when you start adding data to the table.
1080       *
1081       */
1082      function start_output() {
1083          $this->started_output = true;
1084          if ($this->exportclass!==null) {
1085              $this->exportclass->start_table($this->sheettitle);
1086              $this->exportclass->output_headers($this->headers);
1087          } else {
1088              $this->start_html();
1089              $this->print_headers();
1090              echo html_writer::start_tag('tbody');
1091          }
1092      }
1093  
1094      /**
1095       * This function is not part of the public api.
1096       */
1097      function print_row($row, $classname = '') {
1098          echo $this->get_row_html($row, $classname);
1099      }
1100  
1101      /**
1102       * Generate html code for the passed row.
1103       *
1104       * @param array $row Row data.
1105       * @param string $classname classes to add.
1106       *
1107       * @return string $html html code for the row passed.
1108       */
1109      public function get_row_html($row, $classname = '') {
1110          static $suppress_lastrow = NULL;
1111          $rowclasses = array();
1112  
1113          if ($classname) {
1114              $rowclasses[] = $classname;
1115          }
1116  
1117          $rowid = $this->uniqueid . '_r' . $this->currentrow;
1118          $html = '';
1119  
1120          $html .= html_writer::start_tag('tr', array('class' => implode(' ', $rowclasses), 'id' => $rowid));
1121  
1122          // If we have a separator, print it
1123          if ($row === NULL) {
1124              $colcount = count($this->columns);
1125              $html .= html_writer::tag('td', html_writer::tag('div', '',
1126                      array('class' => 'tabledivider')), array('colspan' => $colcount));
1127  
1128          } else {
1129              $html .= $this->get_row_cells_html($rowid, $row, $suppress_lastrow);
1130          }
1131  
1132          $html .= html_writer::end_tag('tr');
1133  
1134          $suppress_enabled = array_sum($this->column_suppress);
1135          if ($suppress_enabled) {
1136              $suppress_lastrow = $row;
1137          }
1138          $this->currentrow++;
1139          return $html;
1140      }
1141  
1142      /**
1143       * Generate html code for the row cells.
1144       *
1145       * @param string $rowid
1146       * @param array $row
1147       * @param array|null $suppresslastrow
1148       * @return string
1149       */
1150      public function get_row_cells_html(string $rowid, array $row, ?array $suppresslastrow): string {
1151          $html = '';
1152          $colbyindex = array_flip($this->columns);
1153          foreach ($row as $index => $data) {
1154              $column = $colbyindex[$index];
1155  
1156              $columnattributes = $this->columnsattributes[$column] ?? [];
1157              if (isset($columnattributes['class'])) {
1158                  $this->column_class($column, $columnattributes['class']);
1159                  unset($columnattributes['class']);
1160              }
1161  
1162              $attributes = [
1163                  'class' => "cell c{$index}" . $this->column_class[$column],
1164                  'id' => "{$rowid}_c{$index}",
1165                  'style' => $this->make_styles_string($this->column_style[$column]),
1166              ];
1167  
1168              $celltype = 'td';
1169              if ($this->headercolumn && $column == $this->headercolumn) {
1170                  $celltype = 'th';
1171                  $attributes['scope'] = 'row';
1172              }
1173  
1174              $attributes += $columnattributes;
1175  
1176              if (empty($this->prefs['collapse'][$column])) {
1177                  if ($this->column_suppress[$column] && $suppresslastrow !== null && $suppresslastrow[$index] === $data) {
1178                      $content = '&nbsp;';
1179                  } else {
1180                      $content = $data;
1181                  }
1182              } else {
1183                  $content = '&nbsp;';
1184              }
1185  
1186              $html .= html_writer::tag($celltype, $content, $attributes);
1187          }
1188          return $html;
1189      }
1190  
1191      /**
1192       * This function is not part of the public api.
1193       */
1194      function finish_html() {
1195          global $OUTPUT, $PAGE;
1196  
1197          if (!$this->started_output) {
1198              //no data has been added to the table.
1199              $this->print_nothing_to_display();
1200  
1201          } else {
1202              // Print empty rows to fill the table to the current pagesize.
1203              // This is done so the header aria-controls attributes do not point to
1204              // non existant elements.
1205              $emptyrow = array_fill(0, count($this->columns), '');
1206              while ($this->currentrow < $this->pagesize) {
1207                  $this->print_row($emptyrow, 'emptyrow');
1208              }
1209  
1210              echo html_writer::end_tag('tbody');
1211              echo html_writer::end_tag('table');
1212              echo html_writer::end_tag('div');
1213              $this->wrap_html_finish();
1214  
1215              // Paging bar
1216              if(in_array(TABLE_P_BOTTOM, $this->showdownloadbuttonsat)) {
1217                  echo $this->download_buttons();
1218              }
1219  
1220              if($this->use_pages) {
1221                  $pagingbar = new paging_bar($this->totalrows, $this->currpage, $this->pagesize, $this->baseurl);
1222                  $pagingbar->pagevar = $this->request[TABLE_VAR_PAGE];
1223                  echo $OUTPUT->render($pagingbar);
1224              }
1225  
1226              // Render the dynamic table footer.
1227              echo $this->get_dynamic_table_html_end();
1228          }
1229      }
1230  
1231      /**
1232       * Generate the HTML for the collapse/uncollapse icon. This is a helper method
1233       * used by {@link print_headers()}.
1234       * @param string $column the column name, index into various names.
1235       * @param int $index numerical index of the column.
1236       * @return string HTML fragment.
1237       */
1238      protected function show_hide_link($column, $index) {
1239          global $OUTPUT;
1240          // Some headers contain <br /> tags, do not include in title, hence the
1241          // strip tags.
1242  
1243          $ariacontrols = '';
1244          for ($i = 0; $i < $this->pagesize; $i++) {
1245              $ariacontrols .= $this->uniqueid . '_r' . $i . '_c' . $index . ' ';
1246          }
1247  
1248          $ariacontrols = trim($ariacontrols);
1249  
1250          if (!empty($this->prefs['collapse'][$column])) {
1251              $linkattributes = [
1252                  'title' => get_string('show') . ' ' . strip_tags($this->headers[$index]),
1253                  'aria-expanded' => 'false',
1254                  'aria-controls' => $ariacontrols,
1255                  'data-action' => 'show',
1256                  'data-column' => $column,
1257                  'role' => 'button',
1258              ];
1259              return html_writer::link($this->baseurl->out(false, array($this->request[TABLE_VAR_SHOW] => $column)),
1260                      $OUTPUT->pix_icon('t/switch_plus', null), $linkattributes);
1261  
1262          } else if ($this->headers[$index] !== NULL) {
1263              $linkattributes = [
1264                  'title' => get_string('hide') . ' ' . strip_tags($this->headers[$index]),
1265                  'aria-expanded' => 'true',
1266                  'aria-controls' => $ariacontrols,
1267                  'data-action' => 'hide',
1268                  'data-column' => $column,
1269                  'role' => 'button',
1270              ];
1271              return html_writer::link($this->baseurl->out(false, array($this->request[TABLE_VAR_HIDE] => $column)),
1272                      $OUTPUT->pix_icon('t/switch_minus', null), $linkattributes);
1273          }
1274      }
1275  
1276      /**
1277       * This function is not part of the public api.
1278       */
1279      function print_headers() {
1280          global $CFG, $OUTPUT;
1281  
1282          // Set the primary sort column/order where possible, so that sort links/icons are correct.
1283          [
1284              'sortby' => $primarysortcolumn,
1285              'sortorder' => $primarysortorder,
1286          ] = $this->get_primary_sort_order();
1287  
1288          echo html_writer::start_tag('thead');
1289          echo html_writer::start_tag('tr');
1290          foreach ($this->columns as $column => $index) {
1291  
1292              $icon_hide = '';
1293              if ($this->is_collapsible) {
1294                  $icon_hide = $this->show_hide_link($column, $index);
1295              }
1296              switch ($column) {
1297  
1298                  case 'userpic':
1299                      // do nothing, do not display sortable links
1300                      break;
1301  
1302                  default:
1303  
1304                      if (array_search($column, $this->userfullnamecolumns) !== false) {
1305                          // Check the full name display for sortable fields.
1306                          if (has_capability('moodle/site:viewfullnames', $this->get_context())) {
1307                              $nameformat = $CFG->alternativefullnameformat;
1308                          } else {
1309                              $nameformat = $CFG->fullnamedisplay;
1310                          }
1311  
1312                          if ($nameformat == 'language') {
1313                              $nameformat = get_string('fullnamedisplay');
1314                          }
1315  
1316                          $requirednames = order_in_string(\core_user\fields::get_name_fields(), $nameformat);
1317  
1318                          if (!empty($requirednames)) {
1319                              if ($this->is_sortable($column)) {
1320                                  // Done this way for the possibility of more than two sortable full name display fields.
1321                                  $this->headers[$index] = '';
1322                                  foreach ($requirednames as $name) {
1323                                      $sortname = $this->sort_link(get_string($name),
1324                                          $name, $primarysortcolumn === $name, $primarysortorder);
1325                                      $this->headers[$index] .= $sortname . ' / ';
1326                                  }
1327                                  $helpicon = '';
1328                                  if (isset($this->helpforheaders[$index])) {
1329                                      $helpicon = $OUTPUT->render($this->helpforheaders[$index]);
1330                                  }
1331                                  $this->headers[$index] = substr($this->headers[$index], 0, -3) . $helpicon;
1332                              }
1333                          }
1334                      } else if ($this->is_sortable($column)) {
1335                          $helpicon = '';
1336                          if (isset($this->helpforheaders[$index])) {
1337                              $helpicon = $OUTPUT->render($this->helpforheaders[$index]);
1338                          }
1339                          $this->headers[$index] = $this->sort_link($this->headers[$index],
1340                                  $column, $primarysortcolumn == $column, $primarysortorder) . $helpicon;
1341                      }
1342              }
1343  
1344              $attributes = array(
1345                  'class' => 'header c' . $index . $this->column_class[$column],
1346                  'scope' => 'col',
1347              );
1348              if ($this->headers[$index] === NULL) {
1349                  $content = '&nbsp;';
1350              } else if (!empty($this->prefs['collapse'][$column])) {
1351                  $content = $icon_hide;
1352              } else {
1353                  if (is_array($this->column_style[$column])) {
1354                      $attributes['style'] = $this->make_styles_string($this->column_style[$column]);
1355                  }
1356                  $helpicon = '';
1357                  if (isset($this->helpforheaders[$index]) && !$this->is_sortable($column)) {
1358                      $helpicon  = $OUTPUT->render($this->helpforheaders[$index]);
1359                  }
1360                  $content = $this->headers[$index] . $helpicon . html_writer::tag('div',
1361                          $icon_hide, array('class' => 'commands'));
1362              }
1363              echo html_writer::tag('th', $content, $attributes);
1364          }
1365  
1366          echo html_writer::end_tag('tr');
1367          echo html_writer::end_tag('thead');
1368      }
1369  
1370      /**
1371       * Calculate the preferences for sort order based on user-supplied values and get params.
1372       */
1373      protected function set_sorting_preferences(): void {
1374          $sortdata = $this->sortdata;
1375  
1376          if ($sortdata === null) {
1377              $sortdata = $this->prefs['sortby'];
1378  
1379              $sortorder = optional_param($this->request[TABLE_VAR_DIR], $this->sort_default_order, PARAM_INT);
1380              $sortby = optional_param($this->request[TABLE_VAR_SORT], '', PARAM_ALPHANUMEXT);
1381  
1382              if (array_key_exists($sortby, $sortdata)) {
1383                  // This key already exists somewhere. Change its sortorder and bring it to the top.
1384                  unset($sortdata[$sortby]);
1385              }
1386              $sortdata = array_merge([$sortby => $sortorder], $sortdata);
1387          }
1388  
1389          $usernamefields = \core_user\fields::get_name_fields();
1390          $sortdata = array_filter($sortdata, function($sortby) use ($usernamefields) {
1391              $isvalidsort = $sortby && $this->is_sortable($sortby);
1392              $isvalidsort = $isvalidsort && empty($this->prefs['collapse'][$sortby]);
1393              $isrealcolumn = isset($this->columns[$sortby]);
1394              $isfullnamefield = $this->contains_fullname_columns() && in_array($sortby, $usernamefields);
1395  
1396              return $isvalidsort && ($isrealcolumn || $isfullnamefield);
1397          }, ARRAY_FILTER_USE_KEY);
1398  
1399          // Finally, make sure that no more than $this->maxsortkeys are present into the array.
1400          $sortdata = array_slice($sortdata, 0, $this->maxsortkeys);
1401  
1402          // If a default order is defined and it is not in the current list of order by columns, add it at the end.
1403          // This prevents results from being returned in a random order if the only order by column contains equal values.
1404          if (!empty($this->sort_default_column) && !array_key_exists($this->sort_default_column, $sortdata)) {
1405              $sortdata = array_merge($sortdata, [$this->sort_default_column => $this->sort_default_order]);
1406          }
1407  
1408          // Apply the sortdata to the preference.
1409          $this->prefs['sortby'] = $sortdata;
1410      }
1411  
1412      /**
1413       * Fill in the preferences for the initials bar.
1414       */
1415      protected function set_initials_preferences(): void {
1416          $ifirst = $this->ifirst;
1417          $ilast = $this->ilast;
1418  
1419          if ($ifirst === null) {
1420              $ifirst = optional_param($this->request[TABLE_VAR_IFIRST], null, PARAM_RAW);
1421          }
1422  
1423          if ($ilast === null) {
1424              $ilast = optional_param($this->request[TABLE_VAR_ILAST], null, PARAM_RAW);
1425          }
1426  
1427          if (!is_null($ifirst) && ($ifirst === '' || strpos(get_string('alphabet', 'langconfig'), $ifirst) !== false)) {
1428              $this->prefs['i_first'] = $ifirst;
1429          }
1430  
1431          if (!is_null($ilast) && ($ilast === '' || strpos(get_string('alphabet', 'langconfig'), $ilast) !== false)) {
1432              $this->prefs['i_last'] = $ilast;
1433          }
1434  
1435      }
1436  
1437      /**
1438       * Set hide and show preferences.
1439       */
1440      protected function set_hide_show_preferences(): void {
1441  
1442          if ($this->hiddencolumns !== null) {
1443              $this->prefs['collapse'] = array_fill_keys(array_filter($this->hiddencolumns, function($column) {
1444                  return array_key_exists($column, $this->columns);
1445              }), true);
1446          } else {
1447              if ($column = optional_param($this->request[TABLE_VAR_HIDE], '', PARAM_ALPHANUMEXT)) {
1448                  if (isset($this->columns[$column])) {
1449                      $this->prefs['collapse'][$column] = true;
1450                  }
1451              }
1452          }
1453  
1454          if ($column = optional_param($this->request[TABLE_VAR_SHOW], '', PARAM_ALPHANUMEXT)) {
1455              unset($this->prefs['collapse'][$column]);
1456          }
1457  
1458          foreach (array_keys($this->prefs['collapse']) as $column) {
1459              if (array_key_exists($column, $this->prefs['sortby'])) {
1460                  unset($this->prefs['sortby'][$column]);
1461              }
1462          }
1463      }
1464  
1465      /**
1466       * Set the list of hidden columns.
1467       *
1468       * @param array $columns The list of hidden columns.
1469       */
1470      public function set_hidden_columns(array $columns): void {
1471          $this->hiddencolumns = $columns;
1472      }
1473  
1474      /**
1475       * Initialise table preferences.
1476       */
1477      protected function initialise_table_preferences(): void {
1478          global $SESSION;
1479  
1480          // Load any existing user preferences.
1481          if ($this->persistent) {
1482              $this->prefs = json_decode(get_user_preferences("flextable_{$this->uniqueid}", ''), true);
1483              $oldprefs = $this->prefs;
1484          } else if (isset($SESSION->flextable[$this->uniqueid])) {
1485              $this->prefs = $SESSION->flextable[$this->uniqueid];
1486              $oldprefs = $this->prefs;
1487          }
1488  
1489          // Set up default preferences if needed.
1490          if (!$this->prefs || $this->is_resetting_preferences()) {
1491              $this->prefs = [
1492                  'collapse' => [],
1493                  'sortby'   => [],
1494                  'i_first'  => '',
1495                  'i_last'   => '',
1496                  'textsort' => $this->column_textsort,
1497              ];
1498          }
1499  
1500          if (!isset($oldprefs)) {
1501              $oldprefs = $this->prefs;
1502          }
1503  
1504          // Save user preferences if they have changed.
1505          if ($this->is_resetting_preferences()) {
1506              $this->sortdata = null;
1507              $this->ifirst = null;
1508              $this->ilast = null;
1509          }
1510  
1511          if (($showcol = optional_param($this->request[TABLE_VAR_SHOW], '', PARAM_ALPHANUMEXT)) &&
1512              isset($this->columns[$showcol])) {
1513              $this->prefs['collapse'][$showcol] = false;
1514          } else if (($hidecol = optional_param($this->request[TABLE_VAR_HIDE], '', PARAM_ALPHANUMEXT)) &&
1515              isset($this->columns[$hidecol])) {
1516              $this->prefs['collapse'][$hidecol] = true;
1517              if (array_key_exists($hidecol, $this->prefs['sortby'])) {
1518                  unset($this->prefs['sortby'][$hidecol]);
1519              }
1520          }
1521  
1522          $this->set_hide_show_preferences();
1523          $this->set_sorting_preferences();
1524          $this->set_initials_preferences();
1525  
1526          // Now, reduce the width of collapsed columns and remove the width from columns that should be expanded.
1527          foreach (array_keys($this->columns) as $column) {
1528              if (!empty($this->prefs['collapse'][$column])) {
1529                  $this->column_style[$column]['width'] = '10px';
1530              } else {
1531                  unset($this->column_style[$column]['width']);
1532              }
1533          }
1534  
1535          if (empty($this->baseurl)) {
1536              debugging('You should set baseurl when using flexible_table.');
1537              global $PAGE;
1538              $this->baseurl = $PAGE->url;
1539          }
1540  
1541          if ($this->currpage == null) {
1542              $this->currpage = optional_param($this->request[TABLE_VAR_PAGE], 0, PARAM_INT);
1543          }
1544  
1545          $this->save_preferences($oldprefs);
1546      }
1547  
1548      /**
1549       * Save preferences.
1550       *
1551       * @param array $oldprefs Old preferences to compare against.
1552       */
1553      protected function save_preferences($oldprefs): void {
1554          global $SESSION;
1555  
1556          if ($this->prefs != $oldprefs) {
1557              if ($this->persistent) {
1558                  set_user_preference('flextable_' . $this->uniqueid, json_encode($this->prefs));
1559              } else {
1560                  $SESSION->flextable[$this->uniqueid] = $this->prefs;
1561              }
1562          }
1563          unset($oldprefs);
1564      }
1565  
1566      /**
1567       * Set the preferred table sorting attributes.
1568       *
1569       * @param string $sortby The field to sort by.
1570       * @param int $sortorder The sort order.
1571       */
1572      public function set_sortdata(array $sortdata): void {
1573          $this->sortdata = [];
1574          foreach ($sortdata as $sortitem) {
1575              if (!array_key_exists($sortitem['sortby'], $this->sortdata)) {
1576                  $this->sortdata[$sortitem['sortby']] = (int) $sortitem['sortorder'];
1577              }
1578          }
1579      }
1580  
1581      /**
1582       * Get the default per page.
1583       *
1584       * @return int
1585       */
1586      public function get_default_per_page(): int {
1587          return $this->defaultperpage;
1588      }
1589  
1590      /**
1591       * Set the default per page.
1592       *
1593       * @param int $defaultperpage
1594       */
1595      public function set_default_per_page(int $defaultperpage): void {
1596          $this->defaultperpage = $defaultperpage;
1597      }
1598  
1599      /**
1600       * Set the preferred first name initial in an initials bar.
1601       *
1602       * @param string $initial The character to set
1603       */
1604      public function set_first_initial(string $initial): void {
1605          $this->ifirst = $initial;
1606      }
1607  
1608      /**
1609       * Set the preferred last name initial in an initials bar.
1610       *
1611       * @param string $initial The character to set
1612       */
1613      public function set_last_initial(string $initial): void {
1614          $this->ilast = $initial;
1615      }
1616  
1617      /**
1618       * Set the page number.
1619       *
1620       * @param int $pagenumber The page number.
1621       */
1622      public function set_page_number(int $pagenumber): void {
1623          $this->currpage = $pagenumber - 1;
1624      }
1625  
1626      /**
1627       * Generate the HTML for the sort icon. This is a helper method used by {@link sort_link()}.
1628       * @param bool $isprimary whether an icon is needed (it is only needed for the primary sort column.)
1629       * @param int $order SORT_ASC or SORT_DESC
1630       * @return string HTML fragment.
1631       */
1632      protected function sort_icon($isprimary, $order) {
1633          global $OUTPUT;
1634  
1635          if (!$isprimary) {
1636              return '';
1637          }
1638  
1639          if ($order == SORT_ASC) {
1640              return $OUTPUT->pix_icon('t/sort_asc', get_string('asc'));
1641          } else {
1642              return $OUTPUT->pix_icon('t/sort_desc', get_string('desc'));
1643          }
1644      }
1645  
1646      /**
1647       * Generate the correct tool tip for changing the sort order. This is a
1648       * helper method used by {@link sort_link()}.
1649       * @param bool $isprimary whether the is column is the current primary sort column.
1650       * @param int $order SORT_ASC or SORT_DESC
1651       * @return string the correct title.
1652       */
1653      protected function sort_order_name($isprimary, $order) {
1654          if ($isprimary && $order != SORT_ASC) {
1655              return get_string('desc');
1656          } else {
1657              return get_string('asc');
1658          }
1659      }
1660  
1661      /**
1662       * Generate the HTML for the sort link. This is a helper method used by {@link print_headers()}.
1663       * @param string $text the text for the link.
1664       * @param string $column the column name, may be a fake column like 'firstname' or a real one.
1665       * @param bool $isprimary whether the is column is the current primary sort column.
1666       * @param int $order SORT_ASC or SORT_DESC
1667       * @return string HTML fragment.
1668       */
1669      protected function sort_link($text, $column, $isprimary, $order) {
1670          // If we are already sorting by this column, switch direction.
1671          if (array_key_exists($column, $this->prefs['sortby'])) {
1672              $sortorder = $this->prefs['sortby'][$column] == SORT_ASC ? SORT_DESC : SORT_ASC;
1673          } else {
1674              $sortorder = $order;
1675          }
1676  
1677          $params = [
1678              $this->request[TABLE_VAR_SORT] => $column,
1679              $this->request[TABLE_VAR_DIR] => $sortorder,
1680          ];
1681  
1682          return html_writer::link($this->baseurl->out(false, $params),
1683                  $text . get_accesshide(get_string('sortby') . ' ' .
1684                  $text . ' ' . $this->sort_order_name($isprimary, $order)),
1685                  [
1686                      'data-sortable' => $this->is_sortable($column),
1687                      'data-sortby' => $column,
1688                      'data-sortorder' => $sortorder,
1689                      'role' => 'button',
1690                  ]) . ' ' . $this->sort_icon($isprimary, $order);
1691      }
1692  
1693      /**
1694       * Return primary sorting column/order, either the first preferred "sortby" value or defaults defined for the table
1695       *
1696       * @return array
1697       */
1698      protected function get_primary_sort_order(): array {
1699          if (reset($this->prefs['sortby'])) {
1700              return $this->get_sort_order();
1701          }
1702  
1703          return [
1704              'sortby' => $this->sort_default_column,
1705              'sortorder' => $this->sort_default_order,
1706          ];
1707      }
1708  
1709      /**
1710       * Return sorting attributes values.
1711       *
1712       * @return array
1713       */
1714      protected function get_sort_order(): array {
1715          $sortbys = $this->prefs['sortby'];
1716          $sortby = key($sortbys);
1717  
1718          return [
1719              'sortby' => $sortby,
1720              'sortorder' => $sortbys[$sortby],
1721          ];
1722      }
1723  
1724      /**
1725       * Get dynamic class component.
1726       *
1727       * @return string
1728       */
1729      protected function get_component() {
1730          $tableclass = explode("\\", get_class($this));
1731          return reset($tableclass);
1732      }
1733  
1734      /**
1735       * Get dynamic class handler.
1736       *
1737       * @return string
1738       */
1739      protected function get_handler() {
1740          $tableclass = explode("\\", get_class($this));
1741          return end($tableclass);
1742      }
1743  
1744      /**
1745       * Get the dynamic table start wrapper.
1746       * If this is not a dynamic table, then an empty string is returned making this safe to blindly call.
1747       *
1748       * @return string
1749       */
1750      protected function get_dynamic_table_html_start(): string {
1751          if (is_a($this, \core_table\dynamic::class)) {
1752              $sortdata = array_map(function($sortby, $sortorder) {
1753                  return [
1754                      'sortby' => $sortby,
1755                      'sortorder' => $sortorder,
1756                  ];
1757              }, array_keys($this->prefs['sortby']), array_values($this->prefs['sortby']));;
1758  
1759              return html_writer::start_tag('div', [
1760                  'class' => 'table-dynamic position-relative',
1761                  'data-region' => 'core_table/dynamic',
1762                  'data-table-handler' => $this->get_handler(),
1763                  'data-table-component' => $this->get_component(),
1764                  'data-table-uniqueid' => $this->uniqueid,
1765                  'data-table-filters' => json_encode($this->get_filterset()),
1766                  'data-table-sort-data' => json_encode($sortdata),
1767                  'data-table-first-initial' => $this->prefs['i_first'],
1768                  'data-table-last-initial' => $this->prefs['i_last'],
1769                  'data-table-page-number' => $this->currpage + 1,
1770                  'data-table-page-size' => $this->pagesize,
1771                  'data-table-default-per-page' => $this->get_default_per_page(),
1772                  'data-table-hidden-columns' => json_encode(array_keys($this->prefs['collapse'])),
1773                  'data-table-total-rows' => $this->totalrows,
1774              ]);
1775          }
1776  
1777          return '';
1778      }
1779  
1780      /**
1781       * Get the dynamic table end wrapper.
1782       * If this is not a dynamic table, then an empty string is returned making this safe to blindly call.
1783       *
1784       * @return string
1785       */
1786      protected function get_dynamic_table_html_end(): string {
1787          global $PAGE;
1788  
1789          if (is_a($this, \core_table\dynamic::class)) {
1790              $output = '';
1791  
1792              $perpageurl = new moodle_url($PAGE->url);
1793  
1794              // Generate "Show all/Show per page" link.
1795              if ($this->pagesize == TABLE_SHOW_ALL_PAGE_SIZE && $this->totalrows > $this->get_default_per_page()) {
1796                  $perpagesize = $this->get_default_per_page();
1797                  $perpagestring = get_string('showperpage', '', $this->get_default_per_page());
1798              } else if ($this->pagesize < $this->totalrows) {
1799                  $perpagesize = TABLE_SHOW_ALL_PAGE_SIZE;
1800                  $perpagestring = get_string('showall', '', $this->totalrows);
1801              }
1802              if (isset($perpagesize) && isset($perpagestring)) {
1803                  $perpageurl->param('perpage', $perpagesize);
1804                  $output .= html_writer::link(
1805                      $perpageurl,
1806                      $perpagestring,
1807                      [
1808                          'data-action' => 'showcount',
1809                          'data-target-page-size' => $perpagesize,
1810                      ]
1811                  );
1812              }
1813  
1814              $PAGE->requires->js_call_amd('core_table/dynamic', 'init');
1815              $output .= html_writer::end_tag('div');
1816              return $output;
1817          }
1818  
1819          return '';
1820      }
1821  
1822      /**
1823       * This function is not part of the public api.
1824       */
1825      function start_html() {
1826          global $OUTPUT;
1827  
1828          // Render the dynamic table header.
1829          echo $this->get_dynamic_table_html_start();
1830  
1831          // Render button to allow user to reset table preferences.
1832          echo $this->render_reset_button();
1833  
1834          // Do we need to print initial bars?
1835          $this->print_initials_bar();
1836  
1837          // Paging bar
1838          if ($this->use_pages) {
1839              $pagingbar = new paging_bar($this->totalrows, $this->currpage, $this->pagesize, $this->baseurl);
1840              $pagingbar->pagevar = $this->request[TABLE_VAR_PAGE];
1841              echo $OUTPUT->render($pagingbar);
1842          }
1843  
1844          if (in_array(TABLE_P_TOP, $this->showdownloadbuttonsat)) {
1845              echo $this->download_buttons();
1846          }
1847  
1848          $this->wrap_html_start();
1849          // Start of main data table
1850  
1851          echo html_writer::start_tag('div', array('class' => 'no-overflow'));
1852          echo html_writer::start_tag('table', $this->attributes) . $this->render_caption();
1853      }
1854  
1855      /**
1856       * This function set caption for table.
1857       *
1858       * @param string $caption Caption of table.
1859       * @param array|null $captionattributes Caption attributes of table.
1860       */
1861      public function set_caption(string $caption, ?array $captionattributes): void {
1862          $this->caption = $caption;
1863          $this->captionattributes = $captionattributes;
1864      }
1865  
1866      /**
1867       * This function renders a table caption.
1868       *
1869       * @return string $output Caption of table.
1870       */
1871      public function render_caption(): string {
1872          if ($this->caption === null) {
1873              return '';
1874          }
1875  
1876          return html_writer::tag(
1877              'caption',
1878              $this->caption,
1879              $this->captionattributes,
1880          );
1881      }
1882  
1883      /**
1884       * This function is not part of the public api.
1885       * @param array $styles CSS-property => value
1886       * @return string values suitably to go in a style="" attribute in HTML.
1887       */
1888      function make_styles_string($styles) {
1889          if (empty($styles)) {
1890              return null;
1891          }
1892  
1893          $string = '';
1894          foreach($styles as $property => $value) {
1895              $string .= $property . ':' . $value . ';';
1896          }
1897          return $string;
1898      }
1899  
1900      /**
1901       * Generate the HTML for the table preferences reset button.
1902       *
1903       * @return string HTML fragment, empty string if no need to reset
1904       */
1905      protected function render_reset_button() {
1906  
1907          if (!$this->can_be_reset()) {
1908              return '';
1909          }
1910  
1911          $url = $this->baseurl->out(false, array($this->request[TABLE_VAR_RESET] => 1));
1912  
1913          $html  = html_writer::start_div('resettable mdl-right');
1914          $html .= html_writer::link($url, get_string('resettable'), ['role' => 'button']);
1915          $html .= html_writer::end_div();
1916  
1917          return $html;
1918      }
1919  
1920      /**
1921       * Are there some table preferences that can be reset?
1922       *
1923       * If true, then the "reset table preferences" widget should be displayed.
1924       *
1925       * @return bool
1926       */
1927      protected function can_be_reset() {
1928          // Loop through preferences and make sure they are empty or set to the default value.
1929          foreach ($this->prefs as $prefname => $prefval) {
1930              if ($prefname === 'sortby' and !empty($this->sort_default_column)) {
1931                  // Check if the actual sorting differs from the default one.
1932                  if (empty($prefval) or $prefval !== array($this->sort_default_column => $this->sort_default_order)) {
1933                      return true;
1934                  }
1935  
1936              } else if ($prefname === 'collapse' and !empty($prefval)) {
1937                  // Check if there are some collapsed columns (all are expanded by default).
1938                  foreach ($prefval as $columnname => $iscollapsed) {
1939                      if ($iscollapsed) {
1940                          return true;
1941                      }
1942                  }
1943  
1944              } else if (!empty($prefval)) {
1945                  // For all other cases, we just check if some preference is set.
1946                  return true;
1947              }
1948          }
1949  
1950          return false;
1951      }
1952  
1953      /**
1954       * Get the context for the table.
1955       *
1956       * Note: This function _must_ be overridden by dynamic tables to ensure that the context is correctly determined
1957       * from the filterset parameters.
1958       *
1959       * @return context
1960       */
1961      public function get_context(): context {
1962          global $PAGE;
1963  
1964          if (is_a($this, \core_table\dynamic::class)) {
1965              throw new coding_exception('The get_context function must be defined for a dynamic table');
1966          }
1967  
1968          return $PAGE->context;
1969      }
1970  
1971      /**
1972       * Set the filterset in the table class.
1973       *
1974       * The use of filtersets is a requirement for dynamic tables, but can be used by other tables too if desired.
1975       *
1976       * @param filterset $filterset The filterset object to get filters and table parameters from
1977       */
1978      public function set_filterset(filterset $filterset): void {
1979          $this->filterset = $filterset;
1980  
1981          $this->guess_base_url();
1982      }
1983  
1984      /**
1985       * Get the currently defined filterset.
1986       *
1987       * @return filterset
1988       */
1989      public function get_filterset(): ?filterset {
1990          return $this->filterset;
1991      }
1992  
1993      /**
1994       * Get the class used as a filterset.
1995       *
1996       * @return string
1997       */
1998      public static function get_filterset_class(): string {
1999          return static::class . '_filterset';
2000      }
2001  
2002      /**
2003       * Attempt to guess the base URL.
2004       */
2005      public function guess_base_url(): void {
2006          if (is_a($this, \core_table\dynamic::class)) {
2007              throw new coding_exception('The guess_base_url function must be defined for a dynamic table');
2008          }
2009      }
2010  }
2011  
2012  
2013  /**
2014   * @package   moodlecore
2015   * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
2016   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2017   */
2018  class table_sql extends flexible_table {
2019  
2020      public $countsql = NULL;
2021      public $countparams = NULL;
2022      /**
2023       * @var object sql for querying db. Has fields 'fields', 'from', 'where', 'params'.
2024       */
2025      public $sql = NULL;
2026      /**
2027       * @var array|\Traversable Data fetched from the db.
2028       */
2029      public $rawdata = NULL;
2030  
2031      /**
2032       * @var bool Overriding default for this.
2033       */
2034      public $is_sortable    = true;
2035      /**
2036       * @var bool Overriding default for this.
2037       */
2038      public $is_collapsible = true;
2039  
2040      /**
2041       * @param string $uniqueid a string identifying this table.Used as a key in
2042       *                          session  vars.
2043       */
2044      function __construct($uniqueid) {
2045          parent::__construct($uniqueid);
2046          // some sensible defaults
2047          $this->set_attribute('class', 'generaltable generalbox');
2048      }
2049  
2050      /**
2051       * Take the data returned from the db_query and go through all the rows
2052       * processing each col using either col_{columnname} method or other_cols
2053       * method or if other_cols returns NULL then put the data straight into the
2054       * table.
2055       *
2056       * After calling this function, don't forget to call close_recordset.
2057       */
2058      public function build_table() {
2059  
2060          if ($this->rawdata instanceof \Traversable && !$this->rawdata->valid()) {
2061              return;
2062          }
2063          if (!$this->rawdata) {
2064              return;
2065          }
2066  
2067          foreach ($this->rawdata as $row) {
2068              $formattedrow = $this->format_row($row);
2069              $this->add_data_keyed($formattedrow,
2070                  $this->get_row_class($row));
2071          }
2072      }
2073  
2074      /**
2075       * Closes recordset (for use after building the table).
2076       */
2077      public function close_recordset() {
2078          if ($this->rawdata && ($this->rawdata instanceof \core\dml\recordset_walk ||
2079                  $this->rawdata instanceof moodle_recordset)) {
2080              $this->rawdata->close();
2081              $this->rawdata = null;
2082          }
2083      }
2084  
2085      /**
2086       * Get any extra classes names to add to this row in the HTML.
2087       * @param $row array the data for this row.
2088       * @return string added to the class="" attribute of the tr.
2089       */
2090      function get_row_class($row) {
2091          return '';
2092      }
2093  
2094      /**
2095       * This is only needed if you want to use different sql to count rows.
2096       * Used for example when perhaps all db JOINS are not needed when counting
2097       * records. You don't need to call this function the count_sql
2098       * will be generated automatically.
2099       *
2100       * We need to count rows returned by the db seperately to the query itself
2101       * as we need to know how many pages of data we have to display.
2102       */
2103      function set_count_sql($sql, array $params = NULL) {
2104          $this->countsql = $sql;
2105          $this->countparams = $params;
2106      }
2107  
2108      /**
2109       * Set the sql to query the db. Query will be :
2110       *      SELECT $fields FROM $from WHERE $where
2111       * Of course you can use sub-queries, JOINS etc. by putting them in the
2112       * appropriate clause of the query.
2113       */
2114      function set_sql($fields, $from, $where, array $params = array()) {
2115          $this->sql = new stdClass();
2116          $this->sql->fields = $fields;
2117          $this->sql->from = $from;
2118          $this->sql->where = $where;
2119          $this->sql->params = $params;
2120      }
2121  
2122      /**
2123       * Query the db. Store results in the table object for use by build_table.
2124       *
2125       * @param int $pagesize size of page for paginated displayed table.
2126       * @param bool $useinitialsbar do you want to use the initials bar. Bar
2127       * will only be used if there is a fullname column defined for the table.
2128       */
2129      function query_db($pagesize, $useinitialsbar=true) {
2130          global $DB;
2131          if (!$this->is_downloading()) {
2132              if ($this->countsql === NULL) {
2133                  $this->countsql = 'SELECT COUNT(1) FROM '.$this->sql->from.' WHERE '.$this->sql->where;
2134                  $this->countparams = $this->sql->params;
2135              }
2136              $grandtotal = $DB->count_records_sql($this->countsql, $this->countparams);
2137              if ($useinitialsbar && !$this->is_downloading()) {
2138                  $this->initialbars(true);
2139              }
2140  
2141              list($wsql, $wparams) = $this->get_sql_where();
2142              if ($wsql) {
2143                  $this->countsql .= ' AND '.$wsql;
2144                  $this->countparams = array_merge($this->countparams, $wparams);
2145  
2146                  $this->sql->where .= ' AND '.$wsql;
2147                  $this->sql->params = array_merge($this->sql->params, $wparams);
2148  
2149                  $total  = $DB->count_records_sql($this->countsql, $this->countparams);
2150              } else {
2151                  $total = $grandtotal;
2152              }
2153  
2154              $this->pagesize($pagesize, $total);
2155          }
2156  
2157          // Fetch the attempts
2158          $sort = $this->get_sql_sort();
2159          if ($sort) {
2160              $sort = "ORDER BY $sort";
2161          }
2162          $sql = "SELECT
2163                  {$this->sql->fields}
2164                  FROM {$this->sql->from}
2165                  WHERE {$this->sql->where}
2166                  {$sort}";
2167  
2168          if (!$this->is_downloading()) {
2169              $this->rawdata = $DB->get_records_sql($sql, $this->sql->params, $this->get_page_start(), $this->get_page_size());
2170          } else {
2171              $this->rawdata = $DB->get_records_sql($sql, $this->sql->params);
2172          }
2173      }
2174  
2175      /**
2176       * Convenience method to call a number of methods for you to display the
2177       * table.
2178       */
2179      function out($pagesize, $useinitialsbar, $downloadhelpbutton='') {
2180          global $DB;
2181          if (!$this->columns) {
2182              $onerow = $DB->get_record_sql("SELECT {$this->sql->fields} FROM {$this->sql->from} WHERE {$this->sql->where}",
2183                  $this->sql->params, IGNORE_MULTIPLE);
2184              //if columns is not set then define columns as the keys of the rows returned
2185              //from the db.
2186              $this->define_columns(array_keys((array)$onerow));
2187              $this->define_headers(array_keys((array)$onerow));
2188          }
2189          $this->pagesize = $pagesize;
2190          $this->setup();
2191          $this->query_db($pagesize, $useinitialsbar);
2192          $this->build_table();
2193          $this->close_recordset();
2194          $this->finish_output();
2195      }
2196  }
2197  
2198  
2199  /**
2200   * @package   moodlecore
2201   * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
2202   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2203   */
2204  class table_default_export_format_parent {
2205      /**
2206       * @var flexible_table or child class reference pointing to table class
2207       * object from which to export data.
2208       */
2209      var $table;
2210  
2211      /**
2212       * @var bool output started. Keeps track of whether any output has been
2213       * started yet.
2214       */
2215      var $documentstarted = false;
2216  
2217      /**
2218       * Constructor
2219       *
2220       * @param flexible_table $table
2221       */
2222      public function __construct(&$table) {
2223          $this->table =& $table;
2224      }
2225  
2226      /**
2227       * Old syntax of class constructor. Deprecated in PHP7.
2228       *
2229       * @deprecated since Moodle 3.1
2230       */
2231      public function table_default_export_format_parent(&$table) {
2232          debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER);
2233          self::__construct($table);
2234      }
2235  
2236      function set_table(&$table) {
2237          $this->table =& $table;
2238      }
2239  
2240      function add_data($row) {
2241          return false;
2242      }
2243  
2244      function add_seperator() {
2245          return false;
2246      }
2247  
2248      function document_started() {
2249          return $this->documentstarted;
2250      }
2251      /**
2252       * Given text in a variety of format codings, this function returns
2253       * the text as safe HTML or as plain text dependent on what is appropriate
2254       * for the download format. The default removes all tags.
2255       */
2256      function format_text($text, $format=FORMAT_MOODLE, $options=NULL, $courseid=NULL) {
2257          //use some whitespace to indicate where there was some line spacing.
2258          $text = str_replace(array('</p>', "\n", "\r"), '   ', $text);
2259          return html_entity_decode(strip_tags($text), ENT_COMPAT);
2260      }
2261  
2262      /**
2263       * Format a row of data, removing HTML tags and entities from each of the cells
2264       *
2265       * @param array $row
2266       * @return array
2267       */
2268      public function format_data(array $row): array {
2269          return array_map([$this, 'format_text'], $row);
2270      }
2271  }
2272  
2273  /**
2274   * Dataformat exporter
2275   *
2276   * @package    core
2277   * @subpackage tablelib
2278   * @copyright  2016 Brendan Heywood (brendan@catalyst-au.net)
2279   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2280   */
2281  class table_dataformat_export_format extends table_default_export_format_parent {
2282  
2283      /** @var \core\dataformat\base $dataformat */
2284      protected $dataformat;
2285  
2286      /** @var int $rownum */
2287      protected $rownum = 0;
2288  
2289      /** @var array $columns */
2290      protected $columns;
2291  
2292      /**
2293       * Constructor
2294       *
2295       * @param string $table An sql table
2296       * @param string $dataformat type of dataformat for export
2297       */
2298      public function __construct(&$table, $dataformat) {
2299          parent::__construct($table);
2300  
2301          if (ob_get_length()) {
2302              throw new coding_exception("Output can not be buffered before instantiating table_dataformat_export_format");
2303          }
2304  
2305          $classname = 'dataformat_' . $dataformat . '\writer';
2306          if (!class_exists($classname)) {
2307              throw new coding_exception("Unable to locate dataformat/$dataformat/classes/writer.php");
2308          }
2309          $this->dataformat = new $classname;
2310  
2311          // The dataformat export time to first byte could take a while to generate...
2312          set_time_limit(0);
2313  
2314          // Close the session so that the users other tabs in the same session are not blocked.
2315          \core\session\manager::write_close();
2316      }
2317  
2318      /**
2319       * Whether the current dataformat supports export of HTML
2320       *
2321       * @return bool
2322       */
2323      public function supports_html(): bool {
2324          return $this->dataformat->supports_html();
2325      }
2326  
2327      /**
2328       * Start document
2329       *
2330       * @param string $filename
2331       * @param string $sheettitle
2332       */
2333      public function start_document($filename, $sheettitle) {
2334          $this->documentstarted = true;
2335          $this->dataformat->set_filename($filename);
2336          $this->dataformat->send_http_headers();
2337          $this->dataformat->set_sheettitle($sheettitle);
2338          $this->dataformat->start_output();
2339      }
2340  
2341      /**
2342       * Start export
2343       *
2344       * @param string $sheettitle optional spreadsheet worksheet title
2345       */
2346      public function start_table($sheettitle) {
2347          $this->dataformat->set_sheettitle($sheettitle);
2348      }
2349  
2350      /**
2351       * Output headers
2352       *
2353       * @param array $headers
2354       */
2355      public function output_headers($headers) {
2356          $this->columns = $this->format_data($headers);
2357          if (method_exists($this->dataformat, 'write_header')) {
2358              error_log('The function write_header() does not support multiple sheets. In order to support multiple sheets you ' .
2359                  'must implement start_output() and start_sheet() and remove write_header() in your dataformat.');
2360              $this->dataformat->write_header($this->columns);
2361          } else {
2362              $this->dataformat->start_sheet($this->columns);
2363          }
2364      }
2365  
2366      /**
2367       * Add a row of data
2368       *
2369       * @param array $row One record of data
2370       */
2371      public function add_data($row) {
2372          if (!$this->supports_html()) {
2373              $row = $this->format_data($row);
2374          }
2375  
2376          $this->dataformat->write_record($row, $this->rownum++);
2377          return true;
2378      }
2379  
2380      /**
2381       * Finish export
2382       */
2383      public function finish_table() {
2384          if (method_exists($this->dataformat, 'write_footer')) {
2385              error_log('The function write_footer() does not support multiple sheets. In order to support multiple sheets you ' .
2386                  'must implement close_sheet() and close_output() and remove write_footer() in your dataformat.');
2387              $this->dataformat->write_footer($this->columns);
2388          } else {
2389              $this->dataformat->close_sheet($this->columns);
2390          }
2391      }
2392  
2393      /**
2394       * Finish download
2395       */
2396      public function finish_document() {
2397          $this->dataformat->close_output();
2398          exit();
2399      }
2400  }