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