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