Proyectos de Subversion Moodle

Rev

Ir a la última revisión | | Ultima modificación | Ver Log |

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