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