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