Proyectos de Subversion Moodle

Rev

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