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