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 /** 18 * Base class for representing a column. 19 * 20 * @package core_question 21 * @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com} 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 namespace core_question\local\bank; 26 27 /** 28 * Base class for representing a column. 29 * 30 * @copyright 2009 Tim Hunt 31 * @author 2021 Safat Shahin <safatshahin@catalyst-au.net> 32 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 33 */ 34 abstract class column_base extends view_component { 35 36 /** 37 * @var string A separator for joining column attributes together into a unique ID string. 38 */ 39 const ID_SEPARATOR = '-'; 40 41 /** 42 * @var view $qbank the question bank view we are helping to render. 43 */ 44 protected $qbank; 45 46 /** @var bool determine whether the column is td or th. */ 47 protected $isheading = false; 48 49 /** @var bool determine whether the column is visible */ 50 public $isvisible = true; 51 52 /** 53 * Return an instance of this column, based on the column name. 54 * 55 * In the case of the base class, we don't actually use the column name since the class represents one specific column. 56 * However, sub-classes may use the column name as an additional constructor to the parameter. 57 * 58 * @param view $view Question bank view 59 * @param string $columnname The column name for this instance, as returned by {@see get_column_name()} 60 * @return column_base An instance of this class. 61 */ 62 public static function from_column_name(view $view, string $columnname): column_base { 63 return new static($view); 64 } 65 66 /** 67 * Set the column as heading 68 */ 69 public function set_as_heading(): void { 70 $this->isheading = true; 71 } 72 73 /** 74 * Check if the column is an extra row of not. 75 */ 76 public function is_extra_row(): bool { 77 return false; 78 } 79 80 /** 81 * Check if the row has an extra preference to view/hide. 82 */ 83 public function has_preference(): bool { 84 return false; 85 } 86 87 /** 88 * Get if the preference key of the row. 89 */ 90 public function get_preference_key(): string { 91 return ''; 92 } 93 94 /** 95 * Get if the preference of the row. 96 */ 97 public function get_preference(): bool { 98 return false; 99 } 100 101 /** 102 * Output the column header cell. 103 * 104 * @param column_action_base[] $columnactions A list of column actions to include in the header. 105 * @param string $width A CSS width property value. 106 */ 107 public function display_header(array $columnactions = [], string $width = ''): void { 108 global $PAGE; 109 $renderer = $PAGE->get_renderer('core_question', 'bank'); 110 111 $data = []; 112 $data['sortable'] = true; 113 $data['extraclasses'] = $this->get_classes(); 114 $sortable = $this->is_sortable(); 115 $name = str_replace('\\', '__', get_class($this)); 116 $title = $this->get_title(); 117 $tip = $this->get_title_tip(); 118 $links = []; 119 if (is_array($sortable)) { 120 if ($title) { 121 $data['title'] = $title; 122 } 123 foreach ($sortable as $subsort => $details) { 124 $links[] = $this->make_sort_link($name . '-' . $subsort, 125 $details['title'], isset($details['tip']) ? $details['tip'] : '', !empty($details['reverse'])); 126 } 127 $data['sortlinks'] = implode(' / ', $links); 128 } else if ($sortable) { 129 $data['sortlinks'] = $this->make_sort_link($name, $title, $tip); 130 } else { 131 $data['sortable'] = false; 132 $data['tiptitle'] = $title; 133 if ($tip) { 134 $data['sorttip'] = true; 135 $data['tip'] = $tip; 136 } 137 } 138 $help = $this->help_icon(); 139 if ($help) { 140 $data['help'] = $help->export_for_template($renderer); 141 } 142 143 $data['colname'] = $this->get_column_name(); 144 $data['columnid'] = $this->get_column_id(); 145 $data['name'] = $title; 146 $data['class'] = $name; 147 $data['width'] = $width; 148 if (!empty($columnactions)) { 149 $actions = array_map(fn($columnaction) => $columnaction->get_action_menu_link($this), $columnactions); 150 $actionmenu = new \action_menu($actions); 151 $data['actionmenu'] = $actionmenu->export_for_template($renderer); 152 } 153 154 echo $renderer->render_column_header($data); 155 } 156 157 /** 158 * Title for this column. Not used if is_sortable returns an array. 159 */ 160 abstract public function get_title(); 161 162 /** 163 * Use this when get_title() returns 164 * something very short, and you want a longer version as a tool tip. 165 * 166 * @return string a fuller version of the name. 167 */ 168 public function get_title_tip() { 169 return ''; 170 } 171 172 /** 173 * If you return a help icon here, it is shown in the column header after the title. 174 * 175 * @return \help_icon|null help icon to show, if required. 176 */ 177 public function help_icon(): ?\help_icon { 178 return null; 179 } 180 181 /** 182 * Get a link that changes the sort order, and indicates the current sort state. 183 * 184 * @param string $sortname the column to sort on. 185 * @param string $title the link text. 186 * @param string $tip the link tool-tip text. If empty, defaults to title. 187 * @param bool $defaultreverse whether the default sort order for this column is descending, rather than ascending. 188 * @return string 189 */ 190 protected function make_sort_link($sortname, $title, $tip, $defaultreverse = false): string { 191 global $PAGE; 192 $sortdata = []; 193 $currentsort = $this->qbank->get_primary_sort_order($sortname); 194 $newsortreverse = $defaultreverse; 195 if ($currentsort) { 196 $newsortreverse = $currentsort == SORT_ASC; 197 } 198 if (!$tip) { 199 $tip = $title; 200 } 201 if ($newsortreverse) { 202 $tip = get_string('sortbyxreverse', '', $tip); 203 } else { 204 $tip = get_string('sortbyx', '', $tip); 205 } 206 207 $link = $title; 208 if ($currentsort) { 209 $link .= $this->get_sort_icon($currentsort == SORT_DESC); 210 } 211 212 $sortdata['sorturl'] = $this->qbank->new_sort_url($sortname, $newsortreverse); 213 $sortdata['sortname'] = $sortname; 214 $sortdata['sortcontent'] = $link; 215 $sortdata['sorttip'] = $tip; 216 $sortdata['sortorder'] = $newsortreverse ? SORT_DESC : SORT_ASC; 217 $renderer = $PAGE->get_renderer('core_question', 'bank'); 218 return $renderer->render_column_sort($sortdata); 219 220 } 221 222 /** 223 * Get an icon representing the corrent sort state. 224 * @param bool $reverse sort is descending, not ascending. 225 * @return string HTML image tag. 226 */ 227 protected function get_sort_icon($reverse): string { 228 global $OUTPUT; 229 if ($reverse) { 230 return $OUTPUT->pix_icon('t/sort_desc', get_string('desc'), '', ['class' => 'iconsort']); 231 } else { 232 return $OUTPUT->pix_icon('t/sort_asc', get_string('asc'), '', ['class' => 'iconsort']); 233 } 234 } 235 236 /** 237 * Output this column. 238 * @param object $question the row from the $question table, augmented with extra information. 239 * @param string $rowclasses CSS class names that should be applied to this row of output. 240 */ 241 public function display($question, $rowclasses): void { 242 $this->display_start($question, $rowclasses); 243 $this->display_content($question, $rowclasses); 244 $this->display_end($question, $rowclasses); 245 } 246 247 /** 248 * Output the opening column tag. If it is set as heading, it will use <th> tag instead of <td> 249 * 250 * @param \stdClass $question 251 * @param string $rowclasses 252 */ 253 protected function display_start($question, $rowclasses): void { 254 $tag = 'td'; 255 $attr = [ 256 'class' => $this->get_classes(), 257 'data-columnid' => $this->get_column_id(), 258 ]; 259 if ($this->isheading) { 260 $tag = 'th'; 261 $attr['scope'] = 'row'; 262 } 263 echo \html_writer::start_tag($tag, $attr); 264 } 265 266 /** 267 * The CSS classes to apply to every cell in this column. 268 * 269 * @return string 270 */ 271 protected function get_classes(): string { 272 $classes = $this->get_extra_classes(); 273 $classes[] = $this->get_name(); 274 return implode(' ', $classes); 275 } 276 277 /** 278 * Get the internal name for this column. Used as a CSS class name, 279 * and to store information about the current sort. Must match PARAM_ALPHA. 280 * 281 * @return string column name. 282 */ 283 abstract public function get_name(); 284 285 /** 286 * Get the name of this column. This must be unique. 287 * When using the inherited class to make many columns from one parent, 288 * ensure each instance returns a unique value. 289 * 290 * @return string The unique name; 291 */ 292 public function get_column_name() { 293 return (new \ReflectionClass($this))->getShortName(); 294 } 295 296 /** 297 * Return a unique ID for this column object. 298 * 299 * This is constructed using the class name and get_column_name(), which must be unique. 300 * 301 * The combination of these attributes allows the object to be reconstructed, by splitting the ID into its constituent 302 * parts then calling {@see from_column_name()}, like this: 303 * [$class, $columnname] = explode(column_base::ID_SEPARATOR, $columnid, 2); 304 * $column = $class::from_column_name($qbank, $columnname); 305 * Including 2 as the $limit parameter for explode() is a good idea for safely, in case a plugin defines a column with the 306 * ID_SEPARATOR in the column name. 307 * 308 * @return string The column ID. 309 */ 310 final public function get_column_id(): string { 311 return implode(self::ID_SEPARATOR, [static::class, $this->get_column_name()]); 312 } 313 314 /** 315 * Any extra class names you would like applied to every cell in this column. 316 * 317 * @return array 318 */ 319 public function get_extra_classes(): array { 320 return []; 321 } 322 323 /** 324 * Return the default column width in pixels. 325 * 326 * @return int 327 */ 328 public function get_default_width(): int { 329 return 120; 330 } 331 332 /** 333 * Output the contents of this column. 334 * @param object $question the row from the $question table, augmented with extra information. 335 * @param string $rowclasses CSS class names that should be applied to this row of output. 336 */ 337 abstract protected function display_content($question, $rowclasses); 338 339 /** 340 * Output the closing column tag 341 * 342 * @param object $question 343 * @param string $rowclasses 344 */ 345 protected function display_end($question, $rowclasses): void { 346 $tag = 'td'; 347 if ($this->isheading) { 348 $tag = 'th'; 349 } 350 echo \html_writer::end_tag($tag); 351 } 352 353 public function get_extra_joins(): array { 354 return []; 355 } 356 357 public function get_required_fields(): array { 358 return []; 359 } 360 361 /** 362 * If this column requires any aggregated statistics, it should declare that here. 363 * 364 * This is those statistics can be efficiently loaded in bulk. 365 * 366 * The statistics are all loaded just before load_additional_data is called on each column. 367 * The values are then available from $this->qbank->get_aggregate_statistic(...); 368 * 369 * @return string[] the names of the required statistics fields. E.g. ['facility']. 370 */ 371 public function get_required_statistics_fields(): array { 372 return []; 373 } 374 375 /** 376 * If this column needs extra data (e.g. tags) then load that here. 377 * 378 * The extra data should be added to the question object in the array. 379 * Probably a good idea to check that another column has not already 380 * loaded the data you want. 381 * 382 * @param \stdClass[] $questions the questions that will be displayed, indexed by question id. 383 */ 384 public function load_additional_data(array $questions) { 385 } 386 387 /** 388 * Load the tags for each question. 389 * 390 * Helper that can be used from {@see load_additional_data()}; 391 * 392 * @param array $questions 393 */ 394 public function load_question_tags(array $questions): void { 395 $firstquestion = reset($questions); 396 if (isset($firstquestion->tags)) { 397 // Looks like tags are already loaded, so don't do it again. 398 return; 399 } 400 401 // Load the tags. 402 $tagdata = \core_tag_tag::get_items_tags('core_question', 'question', 403 array_keys($questions)); 404 405 // Add them to the question objects. 406 foreach ($tagdata as $questionid => $tags) { 407 $questions[$questionid]->tags = $tags; 408 } 409 } 410 411 /** 412 * Can this column be sorted on? You can return either: 413 * + false for no (the default), 414 * + a field name, if sorting this column corresponds to sorting on that datbase field. 415 * + an array of subnames to sort on as follows 416 * return [ 417 * 'firstname' => ['field' => 'uc.firstname', 'title' => get_string('firstname')], 418 * 'lastname' => ['field' => 'uc.lastname', 'title' => get_string('lastname')], 419 * ]; 420 * As well as field, and field, you can also add 'revers' => 1 if you want the default sort 421 * order to be DESC. 422 * @return mixed as above. 423 */ 424 public function is_sortable() { 425 return false; 426 } 427 428 /** 429 * Helper method for building sort clauses. 430 * @param bool $reverse whether the normal direction should be reversed. 431 * @return string 'ASC' or 'DESC' 432 */ 433 protected function sortorder($reverse): string { 434 if ($reverse) { 435 return ' DESC'; 436 } else { 437 return ' ASC'; 438 } 439 } 440 441 /** 442 * Sorts the expressions. 443 * 444 * @param bool $reverse Whether to sort in the reverse of the default sort order. 445 * @param string $subsort if is_sortable returns an array of subnames, then this will be 446 * one of those. Otherwise will be empty. 447 * @return string some SQL to go in the order by clause. 448 */ 449 public function sort_expression($reverse, $subsort): string { 450 $sortable = $this->is_sortable(); 451 if (is_array($sortable)) { 452 if (array_key_exists($subsort, $sortable)) { 453 return $sortable[$subsort]['field'] . $this->sortorder($reverse); 454 } else { 455 throw new \coding_exception('Unexpected $subsort type: ' . $subsort); 456 } 457 } else if ($sortable) { 458 return $sortable . $this->sortorder($reverse); 459 } else { 460 throw new \coding_exception('sort_expression called on a non-sortable column.'); 461 } 462 } 463 464 /** 465 * Output the column with an example value. 466 * 467 * By default, this will call $this->display() using whatever dummy data is passed in. Columns can override this 468 * to provide example output without requiring valid data. 469 * 470 * @param \stdClass $question the row from the $question table, augmented with extra information. 471 * @param string $rowclasses CSS class names that should be applied to this row of output. 472 */ 473 public function display_preview(\stdClass $question, string $rowclasses): void { 474 $this->display($question, $rowclasses); 475 } 476 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body