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