Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.
   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 in a {@link question_bank_view}.
  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\bank;
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  
  29  /**
  30   * Base class for representing a column in a {@link question_bank_view}.
  31   *
  32   * @copyright 2009 Tim Hunt
  33   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  34   */
  35  abstract class column_base {
  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() {
  58      }
  59  
  60      /**
  61       * Set the column as heading
  62       */
  63      public function set_as_heading() {
  64          $this->isheading = true;
  65      }
  66  
  67      public function is_extra_row() {
  68          return false;
  69      }
  70  
  71      /**
  72       * Output the column header cell.
  73       */
  74      public function display_header() {
  75          echo '<th class="header ' . $this->get_classes() . '" scope="col">';
  76          $sortable = $this->is_sortable();
  77          $name = get_class($this);
  78          $title = $this->get_title();
  79          $tip = $this->get_title_tip();
  80          if (is_array($sortable)) {
  81              if ($title) {
  82                  echo '<div class="title">' . $title . '</div>';
  83              }
  84              $links = array();
  85              foreach ($sortable as $subsort => $details) {
  86                  $links[] = $this->make_sort_link($name . '-' . $subsort,
  87                          $details['title'], isset($details['tip']) ? $details['tip'] : '', !empty($details['reverse']));
  88              }
  89              echo '<div class="sorters">' . implode(' / ', $links) . '</div>';
  90          } else if ($sortable) {
  91              echo $this->make_sort_link($name, $title, $tip);
  92          } else {
  93              if ($tip) {
  94                  echo '<span title="' . $tip . '">';
  95              }
  96              echo $title;
  97              if ($tip) {
  98                  echo '</span>';
  99              }
 100          }
 101          echo "</th>\n";
 102      }
 103  
 104      /**
 105       * Title for this column. Not used if is_sortable returns an array.
 106       */
 107      protected abstract function get_title();
 108  
 109      /**
 110       * @return string a fuller version of the name. Use this when get_title() returns
 111       * something very short, and you want a longer version as a tool tip.
 112       */
 113      protected function get_title_tip() {
 114          return '';
 115      }
 116  
 117      /**
 118       * Get a link that changes the sort order, and indicates the current sort state.
 119       * @param string $sort the column to sort on.
 120       * @param string $title the link text.
 121       * @param string $tip the link tool-tip text. If empty, defaults to title.
 122       * @param bool $defaultreverse whether the default sort order for this column is descending, rather than ascending.
 123       * @return string HTML fragment.
 124       */
 125      protected function make_sort_link($sort, $title, $tip, $defaultreverse = false) {
 126          $currentsort = $this->qbank->get_primary_sort_order($sort);
 127          $newsortreverse = $defaultreverse;
 128          if ($currentsort) {
 129              $newsortreverse = $currentsort > 0;
 130          }
 131          if (!$tip) {
 132              $tip = $title;
 133          }
 134          if ($newsortreverse) {
 135              $tip = get_string('sortbyxreverse', '', $tip);
 136          } else {
 137              $tip = get_string('sortbyx', '', $tip);
 138          }
 139          $link = '<a href="' . $this->qbank->new_sort_url($sort, $newsortreverse) . '" title="' . $tip . '">';
 140          $link .= $title;
 141          if ($currentsort) {
 142              $link .= $this->get_sort_icon($currentsort < 0);
 143          }
 144          $link .= '</a>';
 145          return $link;
 146      }
 147  
 148      /**
 149       * Get an icon representing the corrent sort state.
 150       * @param bool $reverse sort is descending, not ascending.
 151       * @return string HTML image tag.
 152       */
 153      protected function get_sort_icon($reverse) {
 154          global $OUTPUT;
 155          if ($reverse) {
 156              return $OUTPUT->pix_icon('t/sort_desc', get_string('desc'), '', array('class' => 'iconsort'));
 157          } else {
 158              return $OUTPUT->pix_icon('t/sort_asc', get_string('asc'), '', array('class' => 'iconsort'));
 159          }
 160      }
 161  
 162      /**
 163       * Output this column.
 164       * @param object $question the row from the $question table, augmented with extra information.
 165       * @param string $rowclasses CSS class names that should be applied to this row of output.
 166       */
 167      public function display($question, $rowclasses) {
 168          $this->display_start($question, $rowclasses);
 169          $this->display_content($question, $rowclasses);
 170          $this->display_end($question, $rowclasses);
 171      }
 172  
 173      /**
 174       * Output the opening column tag.  If it is set as heading, it will use <th> tag instead of <td>
 175       *
 176       * @param \stdClass $question
 177       * @param string $rowclasses
 178       */
 179      protected function display_start($question, $rowclasses) {
 180          $tag = 'td';
 181          $attr = array('class' => $this->get_classes());
 182          if ($this->isheading) {
 183              $tag = 'th';
 184              $attr['scope'] = 'row';
 185          }
 186          echo \html_writer::start_tag($tag, $attr);
 187      }
 188  
 189      /**
 190       * @return string the CSS classes to apply to every cell in this column.
 191       */
 192      protected function get_classes() {
 193          $classes = $this->get_extra_classes();
 194          $classes[] = $this->get_name();
 195          return implode(' ', $classes);
 196      }
 197  
 198      /**
 199       * Get the internal name for this column. Used as a CSS class name,
 200       * and to store information about the current sort. Must match PARAM_ALPHA.
 201       *
 202       * @return string column name.
 203       */
 204      public abstract function get_name();
 205  
 206      /**
 207       * @return array any extra class names you would like applied to every cell in this column.
 208       */
 209      public function get_extra_classes() {
 210          return array();
 211      }
 212  
 213      /**
 214       * Output the contents of 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      protected abstract function display_content($question, $rowclasses);
 219  
 220      /**
 221       * Output the closing column tag
 222       *
 223       * @param object $question
 224       * @param string $rowclasses
 225       */
 226      protected function display_end($question, $rowclasses) {
 227          $tag = 'td';
 228          if ($this->isheading) {
 229              $tag = 'th';
 230          }
 231          echo \html_writer::end_tag($tag);
 232      }
 233  
 234      /**
 235       * Return an array 'table_alias' => 'JOIN clause' to bring in any data that
 236       * this column required.
 237       *
 238       * The return values for all the columns will be checked. It is OK if two
 239       * columns join in the same table with the same alias and identical JOIN clauses.
 240       * If to columns try to use the same alias with different joins, you get an error.
 241       * The only table included by default is the question table, which is aliased to 'q'.
 242       *
 243       * It is importnat that your join simply adds additional data (or NULLs) to the
 244       * existing rows of the query. It must not cause additional rows.
 245       *
 246       * @return array 'table_alias' => 'JOIN clause'
 247       */
 248      public function get_extra_joins() {
 249          return array();
 250      }
 251  
 252      /**
 253       * @return array fields required. use table alias 'q' for the question table, or one of the
 254       * ones from get_extra_joins. Every field requested must specify a table prefix.
 255       */
 256      public function get_required_fields() {
 257          return array();
 258      }
 259  
 260      /**
 261       * If this column needs extra data (e.g. tags) then load that here.
 262       *
 263       * The extra data should be added to the question object in the array.
 264       * Probably a good idea to check that another column has not already
 265       * loaded the data you want.
 266       *
 267       * @param \stdClass[] $questions the questions that will be displayed.
 268       */
 269      public function load_additional_data(array $questions) {
 270      }
 271  
 272      /**
 273       * Load the tags for each question.
 274       *
 275       * Helper that can be used from {@link load_additional_data()};
 276       *
 277       * @param array $questions
 278       */
 279      public function load_question_tags(array $questions) {
 280          $firstquestion = reset($questions);
 281          if (isset($firstquestion->tags)) {
 282              // Looks like tags are already loaded, so don't do it again.
 283              return;
 284          }
 285  
 286          // Load the tags.
 287          $tagdata = \core_tag_tag::get_items_tags('core_question', 'question',
 288                  array_keys($questions));
 289  
 290          // Add them to the question objects.
 291          foreach ($tagdata as $questionid => $tags) {
 292              $questions[$questionid]->tags = $tags;
 293          }
 294      }
 295  
 296      /**
 297       * Can this column be sorted on? You can return either:
 298       *  + false for no (the default),
 299       *  + a field name, if sorting this column corresponds to sorting on that datbase field.
 300       *  + an array of subnames to sort on as follows
 301       *  return array(
 302       *      'firstname' => array('field' => 'uc.firstname', 'title' => get_string('firstname')),
 303       *      'lastname' => array('field' => 'uc.lastname', 'title' => get_string('lastname')),
 304       *  );
 305       * As well as field, and field, you can also add 'revers' => 1 if you want the default sort
 306       * order to be DESC.
 307       * @return mixed as above.
 308       */
 309      public function is_sortable() {
 310          return false;
 311      }
 312  
 313      /**
 314       * Helper method for building sort clauses.
 315       * @param bool $reverse whether the normal direction should be reversed.
 316       * @return string 'ASC' or 'DESC'
 317       */
 318      protected function sortorder($reverse) {
 319          if ($reverse) {
 320              return ' DESC';
 321          } else {
 322              return ' ASC';
 323          }
 324      }
 325  
 326      /**
 327       * @param bool $reverse Whether to sort in the reverse of the default sort order.
 328       * @param string $subsort if is_sortable returns an array of subnames, then this will be
 329       *      one of those. Otherwise will be empty.
 330       * @return string some SQL to go in the order by clause.
 331       */
 332      public function sort_expression($reverse, $subsort) {
 333          $sortable = $this->is_sortable();
 334          if (is_array($sortable)) {
 335              if (array_key_exists($subsort, $sortable)) {
 336                  return $sortable[$subsort]['field'] . $this->sortorder($reverse);
 337              } else {
 338                  throw new \coding_exception('Unexpected $subsort type: ' . $subsort);
 339              }
 340          } else if ($sortable) {
 341              return $sortable . $this->sortorder($reverse);
 342          } else {
 343              throw new \coding_exception('sort_expression called on a non-sortable column.');
 344          }
 345      }
 346  }