Search moodle.org's
Developer Documentation

See Release Notes

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

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

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