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