See Release Notes
Long Term Support Release
Differences Between: [Versions 400 and 401] [Versions 401 and 403]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 declare(strict_types=1); 18 19 namespace core_reportbuilder\table; 20 21 use core\output\notification; 22 use html_writer; 23 use moodle_exception; 24 use moodle_url; 25 use stdClass; 26 use core_reportbuilder\manager; 27 use core_reportbuilder\local\models\report; 28 use core_reportbuilder\local\report\column; 29 use core_reportbuilder\output\column_aggregation_editable; 30 use core_reportbuilder\output\column_heading_editable; 31 32 /** 33 * Custom report dynamic table class 34 * 35 * @package core_reportbuilder 36 * @copyright 2021 David Matamoros <davidmc@moodle.com> 37 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 38 */ 39 class custom_report_table extends base_report_table { 40 41 /** @var string Unique ID prefix for the table */ 42 private const UNIQUEID_PREFIX = 'custom-report-table-'; 43 44 /** @var bool Whether report is being edited (we don't want user filters/sorting to be applied when editing) */ 45 protected const REPORT_EDITING = true; 46 47 /** @var float $querytimestart Time we began executing table SQL */ 48 private $querytimestart = 0.0; 49 50 /** 51 * Table constructor. Note that the passed unique ID value must match the pattern "custom-report-table-(\d+)" so that 52 * dynamic updates continue to load the same report 53 * 54 * @param string $uniqueid 55 * @param string $download 56 * @throws moodle_exception For invalid unique ID 57 */ 58 public function __construct(string $uniqueid, string $download = '') { 59 if (!preg_match('/^' . self::UNIQUEID_PREFIX . '(?<id>\d+)$/', $uniqueid, $matches)) { 60 throw new moodle_exception('invalidcustomreportid', 'core_reportbuilder', '', null, $uniqueid); 61 } 62 63 parent::__construct($uniqueid); 64 65 $this->define_baseurl(new moodle_url('/reportbuilder/edit.php', ['id' => $matches['id']])); 66 67 // Load the report persistent, and accompanying report instance. 68 $this->persistent = new report($matches['id']); 69 $this->report = manager::get_report_from_persistent($this->persistent); 70 71 $fields = $groupby = []; 72 $maintable = $this->report->get_main_table(); 73 $maintablealias = $this->report->get_main_table_alias(); 74 $joins = $this->report->get_joins(); 75 [$where, $params] = $this->report->get_base_condition(); 76 77 $this->set_attribute('data-region', 'reportbuilder-table'); 78 $this->set_attribute('class', $this->attributes['class'] . ' reportbuilder-table'); 79 80 // Download options. 81 $this->showdownloadbuttonsat = [TABLE_P_BOTTOM]; 82 $this->is_downloading($download ?? null, $this->persistent->get_formatted_name()); 83 84 // Retrieve all report columns, exit early if there are none. Defining empty columns prevents errors during out(). 85 $columns = $this->get_active_columns(); 86 if (empty($columns)) { 87 $this->init_sql("{$maintablealias}.*", "{{$maintable}} {$maintablealias}", $joins, '1=0', []); 88 $this->define_columns([0]); 89 return; 90 } 91 92 // If we are aggregating any columns, we should group by the remaining ones. 93 $aggregatedcolumns = array_filter($columns, static function(column $column): bool { 94 return !empty($column->get_aggregation()); 95 }); 96 97 // Also take account of the report setting to show unique rows (only if no columns are being aggregated). 98 $hasaggregatedcolumns = !empty($aggregatedcolumns); 99 $showuniquerows = !$hasaggregatedcolumns && $this->persistent->get('uniquerows'); 100 101 $columnheaders = $columnsattributes = []; 102 foreach ($columns as $column) { 103 $columnheading = $column->get_persistent()->get_formatted_heading($this->report->get_context()); 104 $columnheaders[$column->get_column_alias()] = $columnheading !== '' ? $columnheading : $column->get_title(); 105 106 // We need to determine for each column whether we should group by it's fields, to support aggregation. 107 $columnaggregation = $column->get_aggregation(); 108 if ($showuniquerows || ($hasaggregatedcolumns && empty($columnaggregation))) { 109 $groupby = array_merge($groupby, $column->get_groupby_sql()); 110 } 111 112 // Add each columns fields, joins and params to our report. 113 $fields = array_merge($fields, $column->get_fields()); 114 $joins = array_merge($joins, $column->get_joins()); 115 $params = array_merge($params, $column->get_params()); 116 117 // Disable sorting for some columns. 118 if (!$column->get_is_sortable()) { 119 $this->no_sorting($column->get_column_alias()); 120 } 121 122 // Add column attributes needed for card view. 123 $settings = $this->report->get_settings_values(); 124 $showfirsttitle = $settings['cardview_showfirsttitle'] ?? false; 125 $visiblecolumns = max($settings['cardview_visiblecolumns'] ?? 1, count($this->columns)); 126 if ($showfirsttitle || $column->get_persistent()->get('columnorder') > 1) { 127 $column->add_attributes(['data-cardtitle' => $columnheaders[$column->get_column_alias()]]); 128 } 129 if ($column->get_persistent()->get('columnorder') > $visiblecolumns) { 130 $column->add_attributes(['data-cardviewhidden' => '']); 131 } 132 133 // Generate column attributes to be included in each cell. 134 $columnsattributes[$column->get_column_alias()] = $column->get_attributes(); 135 } 136 137 $this->define_columns(array_keys($columnheaders)); 138 $this->define_headers(array_values($columnheaders)); 139 140 // Add column attributes to the table. 141 $this->set_columnsattributes($columnsattributes); 142 143 // Table configuration. 144 $this->initialbars(false); 145 $this->collapsible(false); 146 $this->pageable(true); 147 $this->set_default_per_page($this->report->get_default_per_page()); 148 149 // Initialise table SQL properties. 150 $this->set_report_editing(static::REPORT_EDITING); 151 152 $fieldsql = implode(', ', $fields); 153 $this->init_sql($fieldsql, "{{$maintable}} {$maintablealias}", $joins, $where, $params, $groupby); 154 } 155 156 /** 157 * Return a new instance of the class for given report ID 158 * 159 * @param int $reportid 160 * @param string $download 161 * @return static 162 */ 163 public static function create(int $reportid, string $download = ''): self { 164 return new static(self::UNIQUEID_PREFIX . $reportid, $download); 165 } 166 167 /** 168 * Get user preferred sort columns, overriding those of parent. If user has no preferences then use report defaults 169 * 170 * @return array 171 */ 172 public function get_sort_columns(): array { 173 $sortcolumns = parent::get_sort_columns(); 174 175 if ($this->editing || empty($sortcolumns)) { 176 $sortcolumns = []; 177 $columns = $this->get_active_columns(); 178 179 // We need to sort the columns by the configured sorting order. 180 usort($columns, static function(column $a, column $b): int { 181 return ($a->get_persistent()->get('sortorder') < $b->get_persistent()->get('sortorder')) ? -1 : 1; 182 }); 183 184 foreach ($columns as $column) { 185 $persistent = $column->get_persistent(); 186 if ($column->get_is_sortable() && $persistent->get('sortenabled')) { 187 $sortcolumns[$column->get_column_alias()] = $persistent->get('sortdirection'); 188 } 189 } 190 } 191 192 return $sortcolumns; 193 } 194 195 /** 196 * Format each row of returned data, executing defined callbacks for the row and each column 197 * 198 * @param array|stdClass $row 199 * @return array 200 */ 201 public function format_row($row) { 202 $columns = $this->get_active_columns(); 203 204 $formattedrow = []; 205 foreach ($columns as $column) { 206 $formattedrow[$column->get_column_alias()] = $column->format_value((array) $row); 207 } 208 209 return $formattedrow; 210 } 211 212 /** 213 * Download is disabled when editing the report 214 * 215 * @return string 216 */ 217 public function download_buttons(): string { 218 return ''; 219 } 220 221 /** 222 * Get the columns of the custom report, returned instances being valid and available for the user 223 * 224 * @return column[] 225 */ 226 protected function get_active_columns(): array { 227 return $this->report->get_active_columns(); 228 } 229 230 /** 231 * Override parent method for printing headers so we can render our custom controls in each cell 232 */ 233 public function print_headers() { 234 global $OUTPUT, $PAGE; 235 236 $columns = $this->get_active_columns(); 237 if (empty($columns)) { 238 return; 239 } 240 241 $columns = array_values($columns); 242 $renderer = $PAGE->get_renderer('core'); 243 244 echo html_writer::start_tag('thead'); 245 echo html_writer::start_tag('tr'); 246 247 foreach ($this->headers as $index => $title) { 248 $column = $columns[$index]; 249 250 $headingeditable = new column_heading_editable(0, $column->get_persistent()); 251 $aggregationeditable = new column_aggregation_editable(0, $column->get_persistent()); 252 253 // Render table header cell, with all editing controls. 254 $headercell = $OUTPUT->render_from_template('core_reportbuilder/table_header_cell', [ 255 'entityname' => $this->report->get_entity_title($column->get_entity_name()), 256 'name' => $column->get_title(), 257 'headingeditable' => $headingeditable->render($renderer), 258 'aggregationeditable' => $aggregationeditable->render($renderer), 259 'movetitle' => get_string('movecolumn', 'core_reportbuilder', $column->get_title()), 260 ]); 261 262 echo html_writer::tag('th', $headercell, [ 263 'class' => 'border-right border-left', 264 'scope' => 'col', 265 'data-region' => 'column-header', 266 'data-column-id' => $column->get_persistent()->get('id'), 267 'data-column-name' => $column->get_title(), 268 'data-column-position' => $index + 1, 269 ]); 270 } 271 272 echo html_writer::end_tag('tr'); 273 echo html_writer::end_tag('thead'); 274 } 275 276 /** 277 * Override print_nothing_to_display to ensure that column headers are always added. 278 */ 279 public function print_nothing_to_display() { 280 global $OUTPUT; 281 282 $this->start_html(); 283 $this->print_headers(); 284 echo html_writer::end_tag('table'); 285 echo html_writer::end_tag('div'); 286 $this->wrap_html_finish(); 287 288 // With the live editing disabled we need to notify user that data is shown only in preview mode. 289 if ($this->editing && !self::show_live_editing()) { 290 $notificationmsg = get_string('customreportsliveeditingdisabled', 'core_reportbuilder'); 291 $notificationtype = notification::NOTIFY_WARNING; 292 } else { 293 $notificationmsg = get_string('nothingtodisplay'); 294 $notificationtype = notification::NOTIFY_INFO; 295 } 296 297 $notification = (new notification($notificationmsg, $notificationtype, false)) 298 ->set_extra_classes(['mt-3']); 299 echo $OUTPUT->render($notification); 300 301 echo $this->get_dynamic_table_html_end(); 302 } 303 304 /** 305 * Provide additional table debugging during editing 306 */ 307 public function wrap_html_finish(): void { 308 global $OUTPUT; 309 310 if ($this->editing && debugging('', DEBUG_DEVELOPER)) { 311 $params = array_map(static function(string $param, $value): array { 312 return ['param' => $param, 'value' => var_export($value, true)]; 313 }, array_keys($this->sql->params), $this->sql->params); 314 315 echo $OUTPUT->render_from_template('core_reportbuilder/local/report/debug', [ 316 'query' => $this->get_table_sql(), 317 'params' => $params, 318 'duration' => $this->querytimestart ? 319 format_time($this->querytimestart - microtime(true)) : null, 320 ]); 321 } 322 } 323 324 /** 325 * Override get_row_cells_html to add an extra cell with the toggle button for card view. 326 * 327 * @param string $rowid 328 * @param array $row 329 * @param array|null $suppresslastrow 330 * @return string 331 */ 332 public function get_row_cells_html(string $rowid, array $row, ?array $suppresslastrow): string { 333 $html = parent::get_row_cells_html($rowid, $row, $suppresslastrow); 334 335 // Add extra 'td' in the row with card toggle button (only visible in card view). 336 $visiblecolumns = $this->report->get_settings_values()['cardview_visiblecolumns'] ?? 1; 337 if ($visiblecolumns < count($this->columns)) { 338 $buttonicon = html_writer::tag('i', '', ['class' => 'fa fa-angle-down']); 339 340 // We need a cleaned version (without tags/entities) of the first row column to use as toggle button. 341 $rowfirstcolumn = strip_tags((string) reset($row)); 342 $buttontitle = $rowfirstcolumn !== '' 343 ? get_string('showhide', 'core_reportbuilder', html_entity_decode($rowfirstcolumn, ENT_COMPAT)) 344 : get_string('showhidecard', 'core_reportbuilder'); 345 346 $button = html_writer::tag('button', $buttonicon, [ 347 'type' => 'button', 348 'class' => 'btn collapsed', 349 'title' => $buttontitle, 350 'data-toggle' => 'collapse', 351 'data-action' => 'toggle-card' 352 ]); 353 $html .= html_writer::tag('td', $button, ['class' => 'card-toggle d-none']); 354 } 355 return $html; 356 } 357 358 /** 359 * Overriding this method to handle live editing setting. 360 * @param int $pagesize 361 * @param bool $useinitialsbar 362 * @param string $downloadhelpbutton 363 */ 364 public function out($pagesize, $useinitialsbar, $downloadhelpbutton = '') { 365 $this->pagesize = $pagesize; 366 $this->setup(); 367 368 // If the live editing setting is disabled, we not need to fetch custom report data except in preview mode. 369 if (!$this->editing || self::show_live_editing()) { 370 $this->querytimestart = microtime(true); 371 $this->query_db($pagesize, $useinitialsbar); 372 $this->build_table(); 373 $this->close_recordset(); 374 } 375 376 $this->finish_output(); 377 } 378 379 /** 380 * Whether or not report data should be included in the table while in editing mode 381 * 382 * @return bool 383 */ 384 private static function show_live_editing(): bool { 385 global $CFG; 386 387 return !empty($CFG->customreportsliveediting); 388 } 389 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body