Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

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