Search moodle.org's
Developer Documentation

See Release Notes

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

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