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