Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

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  }