Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [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  /**
  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 {
  35  
  36      /**
  37       * @var view $qbank the question bank view we are helping to render.
  38       */
  39      protected $qbank;
  40  
  41      /** @var bool determine whether the column is td or th. */
  42      protected $isheading = false;
  43  
  44      /**
  45       * Constructor.
  46       * @param view $qbank the question bank view we are helping to render.
  47       */
  48      public function __construct(view $qbank) {
  49          $this->qbank = $qbank;
  50          $this->init();
  51      }
  52  
  53      /**
  54       * A chance for subclasses to initialise themselves, for example to load lang strings,
  55       * without having to override the constructor.
  56       */
  57      protected function init(): void {
  58      }
  59  
  60      /**
  61       * Set the column as heading
  62       */
  63      public function set_as_heading(): void {
  64          $this->isheading = true;
  65      }
  66  
  67      /**
  68       * Check if the column is an extra row of not.
  69       */
  70      public function is_extra_row(): bool {
  71          return false;
  72      }
  73  
  74      /**
  75       * Check if the row has an extra preference to view/hide.
  76       */
  77      public function has_preference(): bool {
  78          return false;
  79      }
  80  
  81      /**
  82       * Get if the preference key of the row.
  83       */
  84      public function get_preference_key(): string {
  85          return '';
  86      }
  87  
  88      /**
  89       * Get if the preference of the row.
  90       */
  91      public function get_preference(): bool {
  92          return false;
  93      }
  94  
  95      /**
  96       * Output the column header cell.
  97       */
  98      public function display_header(): void {
  99          global $PAGE;
 100          $renderer = $PAGE->get_renderer('core_question', 'bank');
 101  
 102          $data = [];
 103          $data['sortable'] = true;
 104          $data['extraclasses'] = $this->get_classes();
 105          $sortable = $this->is_sortable();
 106          $name = get_class($this);
 107          $title = $this->get_title();
 108          $tip = $this->get_title_tip();
 109          $links = [];
 110          if (is_array($sortable)) {
 111              if ($title) {
 112                  $data['title'] = $title;
 113              }
 114              foreach ($sortable as $subsort => $details) {
 115                  $links[] = $this->make_sort_link($name . '-' . $subsort,
 116                          $details['title'], isset($details['tip']) ? $details['tip'] : '', !empty($details['reverse']));
 117              }
 118              $data['sortlinks'] = implode(' / ', $links);
 119          } else if ($sortable) {
 120              $data['sortlinks'] = $this->make_sort_link($name, $title, $tip);
 121          } else {
 122              $data['sortable'] = false;
 123              $data['tiptitle'] = $title;
 124              if ($tip) {
 125                  $data['sorttip'] = true;
 126                  $data['tip'] = $tip;
 127              }
 128          }
 129          $help = $this->help_icon();
 130          if ($help) {
 131              $data['help'] = $help->export_for_template($renderer);
 132          }
 133  
 134          echo $renderer->render_column_header($data);
 135      }
 136  
 137      /**
 138       * Title for this column. Not used if is_sortable returns an array.
 139       */
 140      abstract public function get_title();
 141  
 142      /**
 143       * Use this when get_title() returns
 144       * something very short, and you want a longer version as a tool tip.
 145       *
 146       * @return string a fuller version of the name.
 147       */
 148      public function get_title_tip() {
 149          return '';
 150      }
 151  
 152      /**
 153       * If you return a help icon here, it is shown in the column header after the title.
 154       *
 155       * @return \help_icon|null help icon to show, if required.
 156       */
 157      public function help_icon(): ?\help_icon {
 158          return null;
 159      }
 160  
 161      /**
 162       * Get a link that changes the sort order, and indicates the current sort state.
 163       * @param string $sort the column to sort on.
 164       * @param string $title the link text.
 165       * @param string $tip the link tool-tip text. If empty, defaults to title.
 166       * @param bool $defaultreverse whether the default sort order for this column is descending, rather than ascending.
 167       * @return string
 168       */
 169      protected function make_sort_link($sort, $title, $tip, $defaultreverse = false): string {
 170          global $PAGE;
 171          $sortdata = [];
 172          $currentsort = $this->qbank->get_primary_sort_order($sort);
 173          $newsortreverse = $defaultreverse;
 174          if ($currentsort) {
 175              $newsortreverse = $currentsort > 0;
 176          }
 177          if (!$tip) {
 178              $tip = $title;
 179          }
 180          if ($newsortreverse) {
 181              $tip = get_string('sortbyxreverse', '', $tip);
 182          } else {
 183              $tip = get_string('sortbyx', '', $tip);
 184          }
 185  
 186          $link = $title;
 187          if ($currentsort) {
 188              $link .= $this->get_sort_icon($currentsort < 0);
 189          }
 190  
 191          $sortdata['sorturl'] = $this->qbank->new_sort_url($sort, $newsortreverse);
 192          $sortdata['sortcontent'] = $link;
 193          $sortdata['sorttip'] = $tip;
 194          $renderer = $PAGE->get_renderer('core_question', 'bank');
 195          return $renderer->render_column_sort($sortdata);
 196  
 197      }
 198  
 199      /**
 200       * Get an icon representing the corrent sort state.
 201       * @param bool $reverse sort is descending, not ascending.
 202       * @return string HTML image tag.
 203       */
 204      protected function get_sort_icon($reverse): string {
 205          global $OUTPUT;
 206          if ($reverse) {
 207              return $OUTPUT->pix_icon('t/sort_desc', get_string('desc'), '', ['class' => 'iconsort']);
 208          } else {
 209              return $OUTPUT->pix_icon('t/sort_asc', get_string('asc'), '', ['class' => 'iconsort']);
 210          }
 211      }
 212  
 213      /**
 214       * Output this column.
 215       * @param object $question the row from the $question table, augmented with extra information.
 216       * @param string $rowclasses CSS class names that should be applied to this row of output.
 217       */
 218      public function display($question, $rowclasses): void {
 219          $this->display_start($question, $rowclasses);
 220          $this->display_content($question, $rowclasses);
 221          $this->display_end($question, $rowclasses);
 222      }
 223  
 224      /**
 225       * Output the opening column tag.  If it is set as heading, it will use <th> tag instead of <td>
 226       *
 227       * @param \stdClass $question
 228       * @param string $rowclasses
 229       */
 230      protected function display_start($question, $rowclasses): void {
 231          $tag = 'td';
 232          $attr = ['class' => $this->get_classes()];
 233          if ($this->isheading) {
 234              $tag = 'th';
 235              $attr['scope'] = 'row';
 236          }
 237          echo \html_writer::start_tag($tag, $attr);
 238      }
 239  
 240      /**
 241       * The CSS classes to apply to every cell in this column.
 242       *
 243       * @return string
 244       */
 245      protected function get_classes(): string {
 246          $classes = $this->get_extra_classes();
 247          $classes[] = $this->get_name();
 248          return implode(' ', $classes);
 249      }
 250  
 251      /**
 252       * Get the internal name for this column. Used as a CSS class name,
 253       * and to store information about the current sort. Must match PARAM_ALPHA.
 254       *
 255       * @return string column name.
 256       */
 257      abstract public function get_name();
 258  
 259      /**
 260       * Get the name of this column. This must be unique.
 261       * When using the inherited class to make many columns from one parent,
 262       * ensure each instance returns a unique value.
 263       *
 264       * @return string The unique name;
 265       */
 266      public function get_column_name() {
 267          return (new \ReflectionClass($this))->getShortName();
 268      }
 269  
 270      /**
 271       * Any extra class names you would like applied to every cell in this column.
 272       *
 273       * @return array
 274       */
 275      public function get_extra_classes(): array {
 276          return [];
 277      }
 278  
 279      /**
 280       * Output the contents of this column.
 281       * @param object $question the row from the $question table, augmented with extra information.
 282       * @param string $rowclasses CSS class names that should be applied to this row of output.
 283       */
 284      abstract protected function display_content($question, $rowclasses);
 285  
 286      /**
 287       * Output the closing column tag
 288       *
 289       * @param object $question
 290       * @param string $rowclasses
 291       */
 292      protected function display_end($question, $rowclasses): void {
 293          $tag = 'td';
 294          if ($this->isheading) {
 295              $tag = 'th';
 296          }
 297          echo \html_writer::end_tag($tag);
 298      }
 299  
 300      /**
 301       * Return an array 'table_alias' => 'JOIN clause' to bring in any data that
 302       * this column required.
 303       *
 304       * The return values for all the columns will be checked. It is OK if two
 305       * columns join in the same table with the same alias and identical JOIN clauses.
 306       * If to columns try to use the same alias with different joins, you get an error.
 307       * The only table included by default is the question table, which is aliased to 'q'.
 308       *
 309       * It is importnat that your join simply adds additional data (or NULLs) to the
 310       * existing rows of the query. It must not cause additional rows.
 311       *
 312       * @return array 'table_alias' => 'JOIN clause'
 313       */
 314      public function get_extra_joins(): array {
 315          return [];
 316      }
 317  
 318      /**
 319       * Use table alias 'q' for the question table, or one of the
 320       * ones from get_extra_joins. Every field requested must specify a table prefix.
 321       *
 322       * @return array fields required.
 323       */
 324      public function get_required_fields(): array {
 325          return [];
 326      }
 327  
 328      /**
 329       * If this column requires any aggregated statistics, it should declare that here.
 330       *
 331       * This is those statistics can be efficiently loaded in bulk.
 332       *
 333       * The statistics are all loaded just before load_additional_data is called on each column.
 334       * The values are then available from $this->qbank->get_aggregate_statistic(...);
 335       *
 336       * @return string[] the names of the required statistics fields. E.g. ['facility'].
 337       */
 338      public function get_required_statistics_fields(): array {
 339          return [];
 340      }
 341  
 342      /**
 343       * If this column needs extra data (e.g. tags) then load that here.
 344       *
 345       * The extra data should be added to the question object in the array.
 346       * Probably a good idea to check that another column has not already
 347       * loaded the data you want.
 348       *
 349       * @param \stdClass[] $questions the questions that will be displayed, indexed by question id.
 350       */
 351      public function load_additional_data(array $questions) {
 352      }
 353  
 354      /**
 355       * Load the tags for each question.
 356       *
 357       * Helper that can be used from {@see load_additional_data()};
 358       *
 359       * @param array $questions
 360       */
 361      public function load_question_tags(array $questions): void {
 362          $firstquestion = reset($questions);
 363          if (isset($firstquestion->tags)) {
 364              // Looks like tags are already loaded, so don't do it again.
 365              return;
 366          }
 367  
 368          // Load the tags.
 369          $tagdata = \core_tag_tag::get_items_tags('core_question', 'question',
 370                  array_keys($questions));
 371  
 372          // Add them to the question objects.
 373          foreach ($tagdata as $questionid => $tags) {
 374              $questions[$questionid]->tags = $tags;
 375          }
 376      }
 377  
 378      /**
 379       * Can this column be sorted on? You can return either:
 380       *  + false for no (the default),
 381       *  + a field name, if sorting this column corresponds to sorting on that datbase field.
 382       *  + an array of subnames to sort on as follows
 383       *  return [
 384       *      'firstname' => ['field' => 'uc.firstname', 'title' => get_string('firstname')],
 385       *      'lastname' => ['field' => 'uc.lastname', 'title' => get_string('lastname')],
 386       *  ];
 387       * As well as field, and field, you can also add 'revers' => 1 if you want the default sort
 388       * order to be DESC.
 389       * @return mixed as above.
 390       */
 391      public function is_sortable() {
 392          return false;
 393      }
 394  
 395      /**
 396       * Helper method for building sort clauses.
 397       * @param bool $reverse whether the normal direction should be reversed.
 398       * @return string 'ASC' or 'DESC'
 399       */
 400      protected function sortorder($reverse): string {
 401          if ($reverse) {
 402              return ' DESC';
 403          } else {
 404              return ' ASC';
 405          }
 406      }
 407  
 408      /**
 409       * Sorts the expressions.
 410       *
 411       * @param bool $reverse Whether to sort in the reverse of the default sort order.
 412       * @param string $subsort if is_sortable returns an array of subnames, then this will be
 413       *      one of those. Otherwise will be empty.
 414       * @return string some SQL to go in the order by clause.
 415       */
 416      public function sort_expression($reverse, $subsort): string {
 417          $sortable = $this->is_sortable();
 418          if (is_array($sortable)) {
 419              if (array_key_exists($subsort, $sortable)) {
 420                  return $sortable[$subsort]['field'] . $this->sortorder($reverse);
 421              } else {
 422                  throw new \coding_exception('Unexpected $subsort type: ' . $subsort);
 423              }
 424          } else if ($sortable) {
 425              return $sortable . $this->sortorder($reverse);
 426          } else {
 427              throw new \coding_exception('sort_expression called on a non-sortable column.');
 428          }
 429      }
 430  
 431  }