MDL-69110 table: Remove commented code
[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                 unset($sortdata[$sortby]);
1310             }
1311             $sortdata = array_merge([$sortby => $sortorder], $sortdata);
1312         }
1314         $usernamefields = get_all_user_name_fields();
1315         $sortdata = array_filter($sortdata, function($sortby) use ($usernamefields) {
1316             $isvalidsort = $sortby && $this->is_sortable($sortby);
1317             $isvalidsort = $isvalidsort && empty($this->prefs['collapse'][$sortby]);
1318             $isrealcolumn = isset($this->columns[$sortby]);
1319             $isfullnamefield = isset($this->columns['fullname']) && in_array($sortby, $usernamefields);
1321             return $isvalidsort && ($isrealcolumn || $isfullnamefield);
1322         }, ARRAY_FILTER_USE_KEY);
1324         // Finally, make sure that no more than $this->maxsortkeys are present into the array.
1325         $sortdata = array_slice($sortdata, 0, $this->maxsortkeys);
1327         // If a default order is defined and it is not in the current list of order by columns, add it at the end.
1328         // This prevents results from being returned in a random order if the only order by column contains equal values.
1329         if (!empty($this->sort_default_column) && !array_key_exists($this->sort_default_column, $sortdata)) {
1330             $sortdata = array_merge($sortdata, [$this->sort_default_column => $this->sort_default_order]);
1331         }
1333         // Apply the sortdata to the preference.
1334         $this->prefs['sortby'] = $sortdata;
1335     }
1337     /**
1338      * Fill in the preferences for the initials bar.
1339      */
1340     protected function set_initials_preferences(): void {
1341         $ifirst = $this->ifirst;
1342         $ilast = $this->ilast;
1344         if ($ifirst === null) {
1345             $ifirst = optional_param($this->request[TABLE_VAR_IFIRST], null, PARAM_RAW);
1346         }
1348         if ($ilast === null) {
1349             $ilast = optional_param($this->request[TABLE_VAR_ILAST], null, PARAM_RAW);
1350         }
1352         if (!is_null($ifirst) && ($ifirst === '' || strpos(get_string('alphabet', 'langconfig'), $ifirst) !== false)) {
1353             $this->prefs['i_first'] = $ifirst;
1354         }
1356         if (!is_null($ilast) && ($ilast === '' || strpos(get_string('alphabet', 'langconfig'), $ilast) !== false)) {
1357             $this->prefs['i_last'] = $ilast;
1358         }
1360     }
1362     /**
1363      * Set hide and show preferences.
1364      */
1365     protected function set_hide_show_preferences(): void {
1367         if ($this->hiddencolumns !== null) {
1368             $this->prefs['collapse'] = array_fill_keys(array_filter($this->hiddencolumns, function($column) {
1369                 return array_key_exists($column, $this->columns);
1370             }), true);
1371         } else {
1372             if ($column = optional_param($this->request[TABLE_VAR_HIDE], '', PARAM_ALPHANUMEXT)) {
1373                 if (isset($this->columns[$column])) {
1374                     $this->prefs['collapse'][$column] = true;
1375                 }
1376             }
1377         }
1379         if ($column = optional_param($this->request[TABLE_VAR_SHOW], '', PARAM_ALPHANUMEXT)) {
1380             unset($this->prefs['collapse'][$column]);
1381         }
1383         foreach (array_keys($this->prefs['collapse']) as $column) {
1384             if (array_key_exists($column, $this->prefs['sortby'])) {
1385                 unset($this->prefs['sortby'][$column]);
1386             }
1387         }
1388     }
1390     /**
1391      * Set the list of hidden columns.
1392      *
1393      * @param array $columns The list of hidden columns.
1394      */
1395     public function set_hidden_columns(array $columns): void {
1396         $this->hiddencolumns = $columns;
1397     }
1399     /**
1400      * Initialise table preferences.
1401      */
1402     protected function initialise_table_preferences(): void {
1403         global $SESSION;
1405         // Load any existing user preferences.
1406         if ($this->persistent) {
1407             $this->prefs = json_decode(get_user_preferences('flextable_' . $this->uniqueid), true);
1408             $oldprefs = $this->prefs;
1409         } else if (isset($SESSION->flextable[$this->uniqueid])) {
1410             $this->prefs = $SESSION->flextable[$this->uniqueid];
1411             $oldprefs = $this->prefs;
1412         }
1414         // Set up default preferences if needed.
1415         if (!$this->prefs || $this->is_resetting_preferences()) {
1416             $this->prefs = [
1417                 'collapse' => [],
1418                 'sortby'   => [],
1419                 'i_first'  => '',
1420                 'i_last'   => '',
1421                 'textsort' => $this->column_textsort,
1422             ];
1423         }
1425         if (!isset($oldprefs)) {
1426             $oldprefs = $this->prefs;
1427         }
1429         // Save user preferences if they have changed.
1430         if ($this->is_resetting_preferences()) {
1431             $this->sortdata = null;
1432             $this->ifirst = null;
1433             $this->ilast = null;
1434         }
1436         if (($showcol = optional_param($this->request[TABLE_VAR_SHOW], '', PARAM_ALPHANUMEXT)) &&
1437             isset($this->columns[$showcol])) {
1438             $this->prefs['collapse'][$showcol] = false;
1439         } else if (($hidecol = optional_param($this->request[TABLE_VAR_HIDE], '', PARAM_ALPHANUMEXT)) &&
1440             isset($this->columns[$hidecol])) {
1441             $this->prefs['collapse'][$hidecol] = true;
1442             if (array_key_exists($hidecol, $this->prefs['sortby'])) {
1443                 unset($this->prefs['sortby'][$hidecol]);
1444             }
1445         }
1447         // Now, update the column attributes for collapsed columns
1448         foreach (array_keys($this->columns) as $column) {
1449             if (!empty($this->prefs['collapse'][$column])) {
1450                 $this->column_style[$column]['width'] = '10px';
1451             }
1452         }
1454         // Now, update the column attributes for collapsed columns
1455         foreach (array_keys($this->columns) as $column) {
1456             if (!empty($this->prefs['collapse'][$column])) {
1457                 $this->column_style[$column]['width'] = '10px';
1458             }
1459         }
1461         $this->set_sorting_preferences();
1462         $this->set_initials_preferences();
1464         if (empty($this->baseurl)) {
1465             debugging('You should set baseurl when using flexible_table.');
1466             global $PAGE;
1467             $this->baseurl = $PAGE->url;
1468         }
1470         if ($this->currpage == null) {
1471             $this->currpage = optional_param($this->request[TABLE_VAR_PAGE], 0, PARAM_INT);
1472         }
1474         $this->save_preferences($oldprefs);
1475     }
1477     /**
1478      * Save preferences.
1479      *
1480      * @param array $oldprefs Old preferences to compare against.
1481      */
1482     protected function save_preferences($oldprefs): void {
1483         global $SESSION;
1485         if ($this->prefs != $oldprefs) {
1486             if ($this->persistent) {
1487                 set_user_preference('flextable_' . $this->uniqueid, json_encode($this->prefs));
1488             } else {
1489                 $SESSION->flextable[$this->uniqueid] = $this->prefs;
1490             }
1491         }
1492         unset($oldprefs);
1493     }
1495     /**
1496      * Set the preferred table sorting attributes.
1497      *
1498      * @param string $sortby The field to sort by.
1499      * @param int $sortorder The sort order.
1500      */
1501     public function set_sortdata(array $sortdata): void {
1502         $this->sortdata = [];
1503         foreach ($sortdata as $sortitem) {
1504             if (!array_key_exists($sortitem['sortby'], $this->sortdata)) {
1505                 $this->sortdata[$sortitem['sortby']] = (int) $sortitem['sortorder'];
1506             }
1507         }
1508     }
1510     /**
1511      * Set the preferred first name initial in an initials bar.
1512      *
1513      * @param string $initial The character to set
1514      */
1515     public function set_first_initial(string $initial): void {
1516         $this->ifirst = $initial;
1517     }
1519     /**
1520      * Set the preferred last name initial in an initials bar.
1521      *
1522      * @param string $initial The character to set
1523      */
1524     public function set_last_initial(string $initial): void {
1525         $this->ilast = $initial;
1526     }
1528     /**
1529      * Set the page number.
1530      *
1531      * @param int $pagenumber The page number.
1532      */
1533     public function set_page_number(int $pagenumber): void {
1534         $this->currpage = $pagenumber - 1;
1535     }
1537     /**
1538      * Generate the HTML for the sort icon. This is a helper method used by {@link sort_link()}.
1539      * @param bool $isprimary whether an icon is needed (it is only needed for the primary sort column.)
1540      * @param int $order SORT_ASC or SORT_DESC
1541      * @return string HTML fragment.
1542      */
1543     protected function sort_icon($isprimary, $order) {
1544         global $OUTPUT;
1546         if (!$isprimary) {
1547             return '';
1548         }
1550         if ($order == SORT_ASC) {
1551             return $OUTPUT->pix_icon('t/sort_asc', get_string('asc'));
1552         } else {
1553             return $OUTPUT->pix_icon('t/sort_desc', get_string('desc'));
1554         }
1555     }
1557     /**
1558      * Generate the correct tool tip for changing the sort order. This is a
1559      * helper method used by {@link sort_link()}.
1560      * @param bool $isprimary whether the is column is the current primary sort column.
1561      * @param int $order SORT_ASC or SORT_DESC
1562      * @return string the correct title.
1563      */
1564     protected function sort_order_name($isprimary, $order) {
1565         if ($isprimary && $order != SORT_ASC) {
1566             return get_string('desc');
1567         } else {
1568             return get_string('asc');
1569         }
1570     }
1572     /**
1573      * Generate the HTML for the sort link. This is a helper method used by {@link print_headers()}.
1574      * @param string $text the text for the link.
1575      * @param string $column the column name, may be a fake column like 'firstname' or a real one.
1576      * @param bool $isprimary whether the is column is the current primary sort column.
1577      * @param int $order SORT_ASC or SORT_DESC
1578      * @return string HTML fragment.
1579      */
1580     protected function sort_link($text, $column, $isprimary, $order) {
1581         // If we are already sorting by this column, switch direction.
1582         if (array_key_exists($column, $this->prefs['sortby'])) {
1583             $sortorder = $this->prefs['sortby'][$column] == SORT_ASC ? SORT_DESC : SORT_ASC;
1584         } else {
1585             $sortorder = $order;
1586         }
1588         $params = [
1589             $this->request[TABLE_VAR_SORT] => $column,
1590             $this->request[TABLE_VAR_DIR] => $sortorder,
1591         ];
1593         return html_writer::link($this->baseurl->out(false, $params),
1594                 $text . get_accesshide(get_string('sortby') . ' ' .
1595                 $text . ' ' . $this->sort_order_name($isprimary, $order)),
1596                 [
1597                     'data-sortable' => $this->is_sortable($column),
1598                     'data-sortby' => $column,
1599                     'data-sortorder' => $sortorder,
1600                 ]) . ' ' . $this->sort_icon($isprimary, $order);
1601     }
1603     /**
1604      * Return sorting attributes values.
1605      *
1606      * @return array
1607      */
1608     protected function get_sort_order(): array {
1609         $sortbys = $this->prefs['sortby'];
1610         $sortby = key($sortbys);
1612         return [
1613             'sortby' => $sortby,
1614             'sortorder' => $sortbys[$sortby],
1615         ];
1616     }
1618     /**
1619      * Get dynamic class component.
1620      *
1621      * @return string
1622      */
1623     protected function get_component() {
1624         $tableclass = explode("\\", get_class($this));
1625         return reset($tableclass);
1626     }
1628     /**
1629      * Get dynamic class handler.
1630      *
1631      * @return string
1632      */
1633     protected function get_handler() {
1634         $tableclass = explode("\\", get_class($this));
1635         return end($tableclass);
1636     }
1638     /**
1639      * Get the dynamic table start wrapper.
1640      * If this is not a dynamic table, then an empty string is returned making this safe to blindly call.
1641      *
1642      * @return string
1643      */
1644     protected function get_dynamic_table_html_start(): string {
1645         if (is_a($this, \core_table\dynamic::class)) {
1646             $sortdata = array_map(function($sortby, $sortorder) {
1647                 return [
1648                     'sortby' => $sortby,
1649                     'sortorder' => $sortorder,
1650                 ];
1651             }, array_keys($this->prefs['sortby']), array_values($this->prefs['sortby']));;
1653             return html_writer::start_tag('div', [
1654                 'class' => 'table-dynamic position-relative',
1655                 'data-region' => 'core_table/dynamic',
1656                 'data-table-handler' => $this->get_handler(),
1657                 'data-table-component' => $this->get_component(),
1658                 'data-table-uniqueid' => $this->uniqueid,
1659                 'data-table-filters' => json_encode($this->get_filterset()),
1660                 'data-table-sort-data' => json_encode($sortdata),
1661                 'data-table-first-initial' => $this->prefs['i_first'],
1662                 'data-table-last-initial' => $this->prefs['i_last'],
1663                 'data-table-page-number' => $this->currpage + 1,
1664                 'data-table-page-size' => $this->pagesize,
1665                 'data-table-hidden-columns' => json_encode(array_keys($this->prefs['collapse'])),
1666                 'data-table-total-rows' => $this->totalrows,
1667             ]);
1668         }
1670         return '';
1671     }
1673     /**
1674      * Get the dynamic table end wrapper.
1675      * If this is not a dynamic table, then an empty string is returned making this safe to blindly call.
1676      *
1677      * @return string
1678      */
1679     protected function get_dynamic_table_html_end(): string {
1680         global $PAGE;
1682         if (is_a($this, \core_table\dynamic::class)) {
1683             $PAGE->requires->js_call_amd('core_table/dynamic', 'init');
1684             return html_writer::end_tag('div');
1685         }
1687         return '';
1688     }
1690     /**
1691      * This function is not part of the public api.
1692      */
1693     function start_html() {
1694         global $OUTPUT;
1696         // Render the dynamic table header.
1697         echo $this->get_dynamic_table_html_start();
1699         // Render button to allow user to reset table preferences.
1700         echo $this->render_reset_button();
1702         // Do we need to print initial bars?
1703         $this->print_initials_bar();
1705         // Paging bar
1706         if ($this->use_pages) {
1707             $pagingbar = new paging_bar($this->totalrows, $this->currpage, $this->pagesize, $this->baseurl);
1708             $pagingbar->pagevar = $this->request[TABLE_VAR_PAGE];
1709             echo $OUTPUT->render($pagingbar);
1710         }
1712         if (in_array(TABLE_P_TOP, $this->showdownloadbuttonsat)) {
1713             echo $this->download_buttons();
1714         }
1716         $this->wrap_html_start();
1717         // Start of main data table
1719         echo html_writer::start_tag('div', array('class' => 'no-overflow'));
1720         echo html_writer::start_tag('table', $this->attributes);
1722     }
1724     /**
1725      * This function is not part of the public api.
1726      * @param array $styles CSS-property => value
1727      * @return string values suitably to go in a style="" attribute in HTML.
1728      */
1729     function make_styles_string($styles) {
1730         if (empty($styles)) {
1731             return null;
1732         }
1734         $string = '';
1735         foreach($styles as $property => $value) {
1736             $string .= $property . ':' . $value . ';';
1737         }
1738         return $string;
1739     }
1741     /**
1742      * Generate the HTML for the table preferences reset button.
1743      *
1744      * @return string HTML fragment, empty string if no need to reset
1745      */
1746     protected function render_reset_button() {
1748         if (!$this->can_be_reset()) {
1749             return '';
1750         }
1752         $url = $this->baseurl->out(false, array($this->request[TABLE_VAR_RESET] => 1));
1754         $html  = html_writer::start_div('resettable mdl-right');
1755         $html .= html_writer::link($url, get_string('resettable'));
1756         $html .= html_writer::end_div();
1758         return $html;
1759     }
1761     /**
1762      * Are there some table preferences that can be reset?
1763      *
1764      * If true, then the "reset table preferences" widget should be displayed.
1765      *
1766      * @return bool
1767      */
1768     protected function can_be_reset() {
1769         // Loop through preferences and make sure they are empty or set to the default value.
1770         foreach ($this->prefs as $prefname => $prefval) {
1771             if ($prefname === 'sortby' and !empty($this->sort_default_column)) {
1772                 // Check if the actual sorting differs from the default one.
1773                 if (empty($prefval) or $prefval !== array($this->sort_default_column => $this->sort_default_order)) {
1774                     return true;
1775                 }
1777             } else if ($prefname === 'collapse' and !empty($prefval)) {
1778                 // Check if there are some collapsed columns (all are expanded by default).
1779                 foreach ($prefval as $columnname => $iscollapsed) {
1780                     if ($iscollapsed) {
1781                         return true;
1782                     }
1783                 }
1785             } else if (!empty($prefval)) {
1786                 // For all other cases, we just check if some preference is set.
1787                 return true;
1788             }
1789         }
1791         return false;
1792     }
1794     /**
1795      * Get the context for the table.
1796      *
1797      * Note: This function _must_ be overridden by dynamic tables to ensure that the context is correctly determined
1798      * from the filterset parameters.
1799      *
1800      * @return context
1801      */
1802     public function get_context(): context {
1803         global $PAGE;
1805         if (is_a($this, \core_table\dynamic::class)) {
1806             throw new coding_exception('The get_context function must be defined for a dynamic table');
1807         }
1809         return $PAGE->context;
1810     }
1812     /**
1813      * Set the filterset in the table class.
1814      *
1815      * The use of filtersets is a requirement for dynamic tables, but can be used by other tables too if desired.
1816      *
1817      * @param filterset $filterset The filterset object to get filters and table parameters from
1818      */
1819     public function set_filterset(filterset $filterset): void {
1820         $this->filterset = $filterset;
1822         $this->guess_base_url();
1823     }
1825     /**
1826      * Get the currently defined filterset.
1827      *
1828      * @return filterset
1829      */
1830     public function get_filterset(): ?filterset {
1831         return $this->filterset;
1832     }
1834     /**
1835      * Attempt to guess the base URL.
1836      */
1837     public function guess_base_url(): void {
1838         if (is_a($this, \core_table\dynamic::class)) {
1839             throw new coding_exception('The guess_base_url function must be defined for a dynamic table');
1840         }
1841     }
1845 /**
1846  * @package   moodlecore
1847  * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
1848  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1849  */
1850 class table_sql extends flexible_table {
1852     public $countsql = NULL;
1853     public $countparams = NULL;
1854     /**
1855      * @var object sql for querying db. Has fields 'fields', 'from', 'where', 'params'.
1856      */
1857     public $sql = NULL;
1858     /**
1859      * @var array|\Traversable Data fetched from the db.
1860      */
1861     public $rawdata = NULL;
1863     /**
1864      * @var bool Overriding default for this.
1865      */
1866     public $is_sortable    = true;
1867     /**
1868      * @var bool Overriding default for this.
1869      */
1870     public $is_collapsible = true;
1872     /**
1873      * @param string $uniqueid a string identifying this table.Used as a key in
1874      *                          session  vars.
1875      */
1876     function __construct($uniqueid) {
1877         parent::__construct($uniqueid);
1878         // some sensible defaults
1879         $this->set_attribute('class', 'generaltable generalbox');
1880     }
1882     /**
1883      * Take the data returned from the db_query and go through all the rows
1884      * processing each col using either col_{columnname} method or other_cols
1885      * method or if other_cols returns NULL then put the data straight into the
1886      * table.
1887      *
1888      * After calling this function, don't forget to call close_recordset.
1889      */
1890     public function build_table() {
1892         if ($this->rawdata instanceof \Traversable && !$this->rawdata->valid()) {
1893             return;
1894         }
1895         if (!$this->rawdata) {
1896             return;
1897         }
1899         foreach ($this->rawdata as $row) {
1900             $formattedrow = $this->format_row($row);
1901             $this->add_data_keyed($formattedrow,
1902                 $this->get_row_class($row));
1903         }
1904     }
1906     /**
1907      * Closes recordset (for use after building the table).
1908      */
1909     public function close_recordset() {
1910         if ($this->rawdata && ($this->rawdata instanceof \core\dml\recordset_walk ||
1911                 $this->rawdata instanceof moodle_recordset)) {
1912             $this->rawdata->close();
1913             $this->rawdata = null;
1914         }
1915     }
1917     /**
1918      * Get any extra classes names to add to this row in the HTML.
1919      * @param $row array the data for this row.
1920      * @return string added to the class="" attribute of the tr.
1921      */
1922     function get_row_class($row) {
1923         return '';
1924     }
1926     /**
1927      * This is only needed if you want to use different sql to count rows.
1928      * Used for example when perhaps all db JOINS are not needed when counting
1929      * records. You don't need to call this function the count_sql
1930      * will be generated automatically.
1931      *
1932      * We need to count rows returned by the db seperately to the query itself
1933      * as we need to know how many pages of data we have to display.
1934      */
1935     function set_count_sql($sql, array $params = NULL) {
1936         $this->countsql = $sql;
1937         $this->countparams = $params;
1938     }
1940     /**
1941      * Set the sql to query the db. Query will be :
1942      *      SELECT $fields FROM $from WHERE $where
1943      * Of course you can use sub-queries, JOINS etc. by putting them in the
1944      * appropriate clause of the query.
1945      */
1946     function set_sql($fields, $from, $where, array $params = array()) {
1947         $this->sql = new stdClass();
1948         $this->sql->fields = $fields;
1949         $this->sql->from = $from;
1950         $this->sql->where = $where;
1951         $this->sql->params = $params;
1952     }
1954     /**
1955      * Query the db. Store results in the table object for use by build_table.
1956      *
1957      * @param int $pagesize size of page for paginated displayed table.
1958      * @param bool $useinitialsbar do you want to use the initials bar. Bar
1959      * will only be used if there is a fullname column defined for the table.
1960      */
1961     function query_db($pagesize, $useinitialsbar=true) {
1962         global $DB;
1963         if (!$this->is_downloading()) {
1964             if ($this->countsql === NULL) {
1965                 $this->countsql = 'SELECT COUNT(1) FROM '.$this->sql->from.' WHERE '.$this->sql->where;
1966                 $this->countparams = $this->sql->params;
1967             }
1968             $grandtotal = $DB->count_records_sql($this->countsql, $this->countparams);
1969             if ($useinitialsbar && !$this->is_downloading()) {
1970                 $this->initialbars(true);
1971             }
1973             list($wsql, $wparams) = $this->get_sql_where();
1974             if ($wsql) {
1975                 $this->countsql .= ' AND '.$wsql;
1976                 $this->countparams = array_merge($this->countparams, $wparams);
1978                 $this->sql->where .= ' AND '.$wsql;
1979                 $this->sql->params = array_merge($this->sql->params, $wparams);
1981                 $total  = $DB->count_records_sql($this->countsql, $this->countparams);
1982             } else {
1983                 $total = $grandtotal;
1984             }
1986             $this->pagesize($pagesize, $total);
1987         }
1989         // Fetch the attempts
1990         $sort = $this->get_sql_sort();
1991         if ($sort) {
1992             $sort = "ORDER BY $sort";
1993         }
1994         $sql = "SELECT
1995                 {$this->sql->fields}
1996                 FROM {$this->sql->from}
1997                 WHERE {$this->sql->where}
1998                 {$sort}";
2000         if (!$this->is_downloading()) {
2001             $this->rawdata = $DB->get_records_sql($sql, $this->sql->params, $this->get_page_start(), $this->get_page_size());
2002         } else {
2003             $this->rawdata = $DB->get_records_sql($sql, $this->sql->params);
2004         }
2005     }
2007     /**
2008      * Convenience method to call a number of methods for you to display the
2009      * table.
2010      */
2011     function out($pagesize, $useinitialsbar, $downloadhelpbutton='') {
2012         global $DB;
2013         if (!$this->columns) {
2014             $onerow = $DB->get_record_sql("SELECT {$this->sql->fields} FROM {$this->sql->from} WHERE {$this->sql->where}",
2015                 $this->sql->params, IGNORE_MULTIPLE);
2016             //if columns is not set then define columns as the keys of the rows returned
2017             //from the db.
2018             $this->define_columns(array_keys((array)$onerow));
2019             $this->define_headers(array_keys((array)$onerow));
2020         }
2021         $this->pagesize = $pagesize;
2022         $this->setup();
2023         $this->query_db($pagesize, $useinitialsbar);
2024         $this->build_table();
2025         $this->close_recordset();
2026         $this->finish_output();
2027     }
2031 /**
2032  * @package   moodlecore
2033  * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
2034  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2035  */
2036 class table_default_export_format_parent {
2037     /**
2038      * @var flexible_table or child class reference pointing to table class
2039      * object from which to export data.
2040      */
2041     var $table;
2043     /**
2044      * @var bool output started. Keeps track of whether any output has been
2045      * started yet.
2046      */
2047     var $documentstarted = false;
2049     /**
2050      * Constructor
2051      *
2052      * @param flexible_table $table
2053      */
2054     public function __construct(&$table) {
2055         $this->table =& $table;
2056     }
2058     /**
2059      * Old syntax of class constructor. Deprecated in PHP7.
2060      *
2061      * @deprecated since Moodle 3.1
2062      */
2063     public function table_default_export_format_parent(&$table) {
2064         debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER);
2065         self::__construct($table);
2066     }
2068     function set_table(&$table) {
2069         $this->table =& $table;
2070     }
2072     function add_data($row) {
2073         return false;
2074     }
2076     function add_seperator() {
2077         return false;
2078     }
2080     function document_started() {
2081         return $this->documentstarted;
2082     }
2083     /**
2084      * Given text in a variety of format codings, this function returns
2085      * the text as safe HTML or as plain text dependent on what is appropriate
2086      * for the download format. The default removes all tags.
2087      */
2088     function format_text($text, $format=FORMAT_MOODLE, $options=NULL, $courseid=NULL) {
2089         //use some whitespace to indicate where there was some line spacing.
2090         $text = str_replace(array('</p>', "\n", "\r"), '   ', $text);
2091         return strip_tags($text);
2092     }
2095 /**
2096  * Dataformat exporter
2097  *
2098  * @package    core
2099  * @subpackage tablelib
2100  * @copyright  2016 Brendan Heywood (brendan@catalyst-au.net)
2101  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2102  */
2103 class table_dataformat_export_format extends table_default_export_format_parent {
2105     /** @var \core\dataformat\base $dataformat */
2106     protected $dataformat;
2108     /** @var $rownum */
2109     protected $rownum = 0;
2111     /** @var $columns */
2112     protected $columns;
2114     /**
2115      * Constructor
2116      *
2117      * @param string $table An sql table
2118      * @param string $dataformat type of dataformat for export
2119      */
2120     public function __construct(&$table, $dataformat) {
2121         parent::__construct($table);
2123         if (ob_get_length()) {
2124             throw new coding_exception("Output can not be buffered before instantiating table_dataformat_export_format");
2125         }
2127         $classname = 'dataformat_' . $dataformat . '\writer';
2128         if (!class_exists($classname)) {
2129             throw new coding_exception("Unable to locate dataformat/$dataformat/classes/writer.php");
2130         }
2131         $this->dataformat = new $classname;
2133         // The dataformat export time to first byte could take a while to generate...
2134         set_time_limit(0);
2136         // Close the session so that the users other tabs in the same session are not blocked.
2137         \core\session\manager::write_close();
2138     }
2140     /**
2141      * Whether the current dataformat supports export of HTML
2142      *
2143      * @return bool
2144      */
2145     public function supports_html(): bool {
2146         return $this->dataformat->supports_html();
2147     }
2149     /**
2150      * Start document
2151      *
2152      * @param string $filename
2153      * @param string $sheettitle
2154      */
2155     public function start_document($filename, $sheettitle) {
2156         $this->documentstarted = true;
2157         $this->dataformat->set_filename($filename);
2158         $this->dataformat->send_http_headers();
2159         $this->dataformat->set_sheettitle($sheettitle);
2160         $this->dataformat->start_output();
2161     }
2163     /**
2164      * Start export
2165      *
2166      * @param string $sheettitle optional spreadsheet worksheet title
2167      */
2168     public function start_table($sheettitle) {
2169         $this->dataformat->set_sheettitle($sheettitle);
2170     }
2172     /**
2173      * Output headers
2174      *
2175      * @param array $headers
2176      */
2177     public function output_headers($headers) {
2178         $this->columns = $headers;
2179         if (method_exists($this->dataformat, 'write_header')) {
2180             error_log('The function write_header() does not support multiple sheets. In order to support multiple sheets you ' .
2181                 'must implement start_output() and start_sheet() and remove write_header() in your dataformat.');
2182             $this->dataformat->write_header($headers);
2183         } else {
2184             $this->dataformat->start_sheet($headers);
2185         }
2186     }
2188     /**
2189      * Add a row of data
2190      *
2191      * @param array $row One record of data
2192      */
2193     public function add_data($row) {
2194         $this->dataformat->write_record($row, $this->rownum++);
2195         return true;
2196     }
2198     /**
2199      * Finish export
2200      */
2201     public function finish_table() {
2202         if (method_exists($this->dataformat, 'write_footer')) {
2203             error_log('The function write_footer() does not support multiple sheets. In order to support multiple sheets you ' .
2204                 'must implement close_sheet() and close_output() and remove write_footer() in your dataformat.');
2205             $this->dataformat->write_footer($this->columns);
2206         } else {
2207             $this->dataformat->close_sheet($this->columns);
2208         }
2209     }
2211     /**
2212      * Finish download
2213      */
2214     public function finish_document() {
2215         $this->dataformat->close_output();
2216         exit();
2217     }