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 400 and 401] [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   * Class to print a view of the question bank.
  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  use core_plugin_manager;
  28  use core_question\bank\search\condition;
  29  use core_question\local\statistics\statistics_bulk_loader;
  30  use qbank_columnsortorder\column_manager;
  31  use qbank_editquestion\editquestion_helper;
  32  
  33  defined('MOODLE_INTERNAL') || die();
  34  
  35  require_once($CFG->dirroot . '/question/editlib.php');
  36  
  37  /**
  38   * This class prints a view of the question bank.
  39   *
  40   * including
  41   *  + Some controls to allow users to to select what is displayed.
  42   *  + A list of questions as a table.
  43   *  + Further controls to do things with the questions.
  44   *
  45   * This class gives a basic view, and provides plenty of hooks where subclasses
  46   * can override parts of the display.
  47   *
  48   * The list of questions presented as a table is generated by creating a list of
  49   * core_question\bank\column objects, one for each 'column' to be displayed. These
  50   * manage
  51   *  + outputting the contents of that column, given a $question object, but also
  52   *  + generating the right fragments of SQL to ensure the necessary data is present,
  53   *    and sorted in the right order.
  54   *  + outputting table headers.
  55   *
  56   * @copyright 2009 Tim Hunt
  57   * @author    2021 Safat Shahin <safatshahin@catalyst-au.net>
  58   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  59   */
  60  class view {
  61  
  62      /**
  63       * Maximum number of sorts allowed.
  64       */
  65      const MAX_SORTS = 3;
  66  
  67      /**
  68       * @var \moodle_url base URL for the current page. Used as the
  69       * basis for making URLs for actions that reload the page.
  70       */
  71      protected $baseurl;
  72  
  73      /**
  74       * @var \moodle_url used as a basis for URLs that edit a question.
  75       */
  76      protected $editquestionurl;
  77  
  78      /**
  79       * @var \core_question\local\bank\question_edit_contexts
  80       */
  81      protected $contexts;
  82  
  83      /**
  84       * @var object|\cm_info|null if we are in a module context, the cm.
  85       */
  86      public $cm;
  87  
  88      /**
  89       * @var object the course we are within.
  90       */
  91      public $course;
  92  
  93      /**
  94       * @var column_base[] these are all the 'columns' that are
  95       * part of the display. Array keys are the class name.
  96       */
  97      protected $requiredcolumns;
  98  
  99      /**
 100       * @var column_base[] these are the 'columns' that are
 101       * actually displayed as a column, in order. Array keys are the class name.
 102       */
 103      protected $visiblecolumns;
 104  
 105      /**
 106       * @var column_base[] these are the 'columns' that are
 107       * actually displayed as an additional row (e.g. question text), in order.
 108       * Array keys are the class name.
 109       */
 110      protected $extrarows;
 111  
 112      /**
 113       * @var array list of column class names for which columns to sort on.
 114       */
 115      protected $sort;
 116  
 117      /**
 118       * @var int page size to use (when we are not showing all questions).
 119       */
 120      protected $pagesize = DEFAULT_QUESTIONS_PER_PAGE;
 121  
 122      /**
 123       * @var int|null id of the a question to highlight in the list (if present).
 124       */
 125      protected $lastchangedid;
 126  
 127      /**
 128       * @var string SQL to count the number of questions matching the current
 129       * search conditions.
 130       */
 131      protected $countsql;
 132  
 133      /**
 134       * @var string SQL to actually load the question data to display.
 135       */
 136      protected $loadsql;
 137  
 138      /**
 139       * @var array params used by $countsql and $loadsql (which currently must be the same).
 140       */
 141      protected $sqlparams;
 142  
 143      /**
 144       * @var ?array Stores all the average statistics that this question bank view needs.
 145       *
 146       * This field gets initialised in {@see display_question_list()}. It is a two dimensional
 147       * $this->loadedstatistics[$questionid][$fieldname] = $average value of that statistics for that question.
 148       * Column classes in qbank plugins can access these values using {@see get_aggregate_statistic()}.
 149       */
 150      protected $loadedstatistics = null;
 151  
 152      /**
 153       * @var condition[] search conditions.
 154       */
 155      protected $searchconditions = [];
 156  
 157      /**
 158       * @var string url of the new question page.
 159       */
 160      public $returnurl;
 161  
 162      /**
 163       * @var bool enable or disable filters while calling the API.
 164       */
 165      public $enablefilters = true;
 166  
 167      /**
 168       * @var array to pass custom filters instead of the specified ones.
 169       */
 170      public $customfilterobjects = null;
 171  
 172      /**
 173       * @var array $bulkactions to identify the bulk actions for the api.
 174       */
 175      public $bulkactions = [];
 176  
 177      /**
 178       * Constructor for view.
 179       *
 180       * @param \core_question\local\bank\question_edit_contexts $contexts
 181       * @param \moodle_url $pageurl
 182       * @param object $course course settings
 183       * @param object $cm (optional) activity settings.
 184       */
 185      public function __construct($contexts, $pageurl, $course, $cm = null) {
 186          $this->contexts = $contexts;
 187          $this->baseurl = $pageurl;
 188          $this->course = $course;
 189          $this->cm = $cm;
 190  
 191          // Create the url of the new question page to forward to.
 192          $this->returnurl = $pageurl->out_as_local_url(false);
 193          $this->editquestionurl = new \moodle_url('/question/bank/editquestion/question.php', ['returnurl' => $this->returnurl]);
 194          if ($this->cm !== null) {
 195              $this->editquestionurl->param('cmid', $this->cm->id);
 196          } else {
 197              $this->editquestionurl->param('courseid', $this->course->id);
 198          }
 199  
 200          $this->lastchangedid = optional_param('lastchanged', 0, PARAM_INT);
 201  
 202          // Possibly the heading part can be removed.
 203          $this->init_columns($this->wanted_columns(), $this->heading_column());
 204          $this->init_sort();
 205          $this->init_search_conditions();
 206          $this->init_bulk_actions();
 207      }
 208  
 209      /**
 210       * Initialize bulk actions.
 211       */
 212      protected function init_bulk_actions(): void {
 213          $plugins = \core_component::get_plugin_list_with_class('qbank', 'plugin_feature', 'plugin_feature.php');
 214          foreach ($plugins as $componentname => $plugin) {
 215              if (!\core\plugininfo\qbank::is_plugin_enabled($componentname)) {
 216                  continue;
 217              }
 218  
 219              $pluginentrypoint = new $plugin();
 220              $bulkactions = $pluginentrypoint->get_bulk_actions();
 221              if (!is_array($bulkactions)) {
 222                  debugging("The method {$componentname}::get_bulk_actions() must return an " .
 223                      "array of bulk actions instead of a single bulk action. " .
 224                      "Please update your implementation of get_bulk_actions() to return an array. " .
 225                      "Check out the qbank_bulkmove plugin for a working example.", DEBUG_DEVELOPER);
 226                  $bulkactions = [$bulkactions];
 227              }
 228  
 229              foreach ($bulkactions as $bulkactionobject) {
 230                  $this->bulkactions[$bulkactionobject->get_key()] = [
 231                      'title' => $bulkactionobject->get_bulk_action_title(),
 232                      'url' => $bulkactionobject->get_bulk_action_url(),
 233                      'capabilities' => $bulkactionobject->get_bulk_action_capabilities()
 234                  ];
 235              }
 236  
 237          }
 238      }
 239  
 240      /**
 241       * Initialize search conditions from plugins
 242       * local_*_get_question_bank_search_conditions() must return an array of
 243       * \core_question\bank\search\condition objects.
 244       */
 245      protected function init_search_conditions(): void {
 246          $searchplugins = get_plugin_list_with_function('local', 'get_question_bank_search_conditions');
 247          foreach ($searchplugins as $component => $function) {
 248              foreach ($function($this) as $searchobject) {
 249                  $this->add_searchcondition($searchobject);
 250              }
 251          }
 252      }
 253  
 254      /**
 255       * Get the list of qbank plugins with available objects for features.
 256       *
 257       * @return array
 258       */
 259      protected function get_question_bank_plugins(): array {
 260          $questionbankclasscolumns = [];
 261          $newpluginclasscolumns = [];
 262          $corequestionbankcolumns = [
 263                  'checkbox_column',
 264                  'question_type_column',
 265                  'question_name_idnumber_tags_column',
 266                  'edit_menu_column',
 267                  'edit_action_column',
 268                  'copy_action_column',
 269                  'tags_action_column',
 270                  'preview_action_column',
 271                  'history_action_column',
 272                  'delete_action_column',
 273                  'export_xml_action_column',
 274                  'question_status_column',
 275                  'version_number_column',
 276                  'creator_name_column',
 277                  'comment_count_column'
 278          ];
 279          if (question_get_display_preference('qbshowtext', 0, PARAM_INT, new \moodle_url(''))) {
 280              $corequestionbankcolumns[] = 'question_text_row';
 281          }
 282  
 283          foreach ($corequestionbankcolumns as $fullname) {
 284              $shortname = $fullname;
 285              if (class_exists('core_question\\local\\bank\\' . $fullname)) {
 286                  $fullname = 'core_question\\local\\bank\\' . $fullname;
 287                  $questionbankclasscolumns[$shortname] = new $fullname($this);
 288              } else {
 289                  $questionbankclasscolumns[$shortname] = '';
 290              }
 291          }
 292          $plugins = \core_component::get_plugin_list_with_class('qbank', 'plugin_feature', 'plugin_feature.php');
 293          foreach ($plugins as $componentname => $plugin) {
 294              $pluginentrypointobject = new $plugin();
 295              $plugincolumnobjects = $pluginentrypointobject->get_question_columns($this);
 296              // Don't need the plugins without column objects.
 297              if (empty($plugincolumnobjects)) {
 298                  unset($plugins[$componentname]);
 299                  continue;
 300              }
 301              foreach ($plugincolumnobjects as $columnobject) {
 302                  $columnname = $columnobject->get_column_name();
 303                  foreach ($corequestionbankcolumns as $corequestionbankcolumn) {
 304                      if (!\core\plugininfo\qbank::is_plugin_enabled($componentname)) {
 305                          unset($questionbankclasscolumns[$columnname]);
 306                          continue;
 307                      }
 308                      // Check if it has custom preference selector to view/hide.
 309                      if ($columnobject->has_preference()) {
 310                          if (!$columnobject->get_preference()) {
 311                              continue;
 312                          }
 313                      }
 314                      if ($corequestionbankcolumn === $columnname) {
 315                          $questionbankclasscolumns[$columnname] = $columnobject;
 316                      } else {
 317                          // Any community plugin for column/action.
 318                          $newpluginclasscolumns[$columnname] = $columnobject;
 319                      }
 320                  }
 321              }
 322          }
 323  
 324          // New plugins added at the end of the array, will change in sorting feature.
 325          foreach ($newpluginclasscolumns as $key => $newpluginclasscolumn) {
 326              $questionbankclasscolumns[$key] = $newpluginclasscolumn;
 327          }
 328  
 329          // Check if qbank_columnsortorder is enabled.
 330          if (array_key_exists('columnsortorder', core_plugin_manager::instance()->get_enabled_plugins('qbank'))) {
 331              $columnorder = new column_manager();
 332              $questionbankclasscolumns = $columnorder->get_sorted_columns($questionbankclasscolumns);
 333          }
 334  
 335          // Mitigate the error in case of any regression.
 336          foreach ($questionbankclasscolumns as $shortname => $questionbankclasscolumn) {
 337              if (!is_object($questionbankclasscolumn)) {
 338                  unset($questionbankclasscolumns[$shortname]);
 339              }
 340          }
 341  
 342          return $questionbankclasscolumns;
 343      }
 344  
 345      /**
 346       * Loads all the available columns.
 347       *
 348       * @return array
 349       */
 350      protected function wanted_columns(): array {
 351          $this->requiredcolumns = [];
 352          $questionbankcolumns = $this->get_question_bank_plugins();
 353          foreach ($questionbankcolumns as $classobject) {
 354              if (empty($classobject)) {
 355                  continue;
 356              }
 357              $this->requiredcolumns[$classobject->get_column_name()] = $classobject;
 358          }
 359  
 360          return $this->requiredcolumns;
 361      }
 362  
 363  
 364      /**
 365       * Check a column object from its name and get the object for sort.
 366       *
 367       * @param string $columnname
 368       */
 369      protected function get_column_type($columnname) {
 370          if (empty($this->requiredcolumns[$columnname])) {
 371              $this->requiredcolumns[$columnname] = new $columnname($this);
 372          }
 373      }
 374  
 375      /**
 376       * Specify the column heading
 377       *
 378       * @return string Column name for the heading
 379       */
 380      protected function heading_column(): string {
 381          return 'qbank_viewquestionname\viewquestionname_column_helper';
 382      }
 383  
 384      /**
 385       * Initializing table columns
 386       *
 387       * @param array $wanted Collection of column names
 388       * @param string $heading The name of column that is set as heading
 389       */
 390      protected function init_columns($wanted, $heading = ''): void {
 391          // If we are using the edit menu column, allow it to absorb all the actions.
 392          foreach ($wanted as $column) {
 393              if ($column instanceof edit_menu_column) {
 394                  $wanted = $column->claim_menuable_columns($wanted);
 395                  break;
 396              }
 397          }
 398  
 399          // Now split columns into real columns and rows.
 400          $this->visiblecolumns = [];
 401          $this->extrarows = [];
 402          foreach ($wanted as $column) {
 403              if ($column->is_extra_row()) {
 404                  $this->extrarows[$column->get_column_name()] = $column;
 405              } else {
 406                  $this->visiblecolumns[$column->get_column_name()] = $column;
 407              }
 408          }
 409  
 410          if (array_key_exists($heading, $this->requiredcolumns)) {
 411              $this->requiredcolumns[$heading]->set_as_heading();
 412          }
 413      }
 414  
 415      /**
 416       * Checks if the column included in the output.
 417       *
 418       * @param string $colname a column internal name.
 419       * @return bool is this column included in the output?
 420       */
 421      public function has_column($colname): bool {
 422          return isset($this->visiblecolumns[$colname]);
 423      }
 424  
 425      /**
 426       * Get the count of the columns.
 427       *
 428       * @return int The number of columns in the table.
 429       */
 430      public function get_column_count(): int {
 431          return count($this->visiblecolumns);
 432      }
 433  
 434      /**
 435       * Get course id.
 436       * @return mixed
 437       */
 438      public function get_courseid() {
 439          return $this->course->id;
 440      }
 441  
 442      /**
 443       * Initialise sorting.
 444       */
 445      protected function init_sort(): void {
 446          $this->init_sort_from_params();
 447          if (empty($this->sort)) {
 448              $this->sort = $this->default_sort();
 449          }
 450      }
 451  
 452      /**
 453       * Deal with a sort name of the form columnname, or colname_subsort by
 454       * breaking it up, validating the bits that are present, and returning them.
 455       * If there is no subsort, then $subsort is returned as ''.
 456       *
 457       * @param string $sort the sort parameter to process.
 458       * @return array [$colname, $subsort].
 459       */
 460      protected function parse_subsort($sort): array {
 461          // Do the parsing.
 462          if (strpos($sort, '-') !== false) {
 463              list($colname, $subsort) = explode('-', $sort, 2);
 464          } else {
 465              $colname = $sort;
 466              $subsort = '';
 467          }
 468          // Validate the column name.
 469          $this->get_column_type($colname);
 470          $column = $this->requiredcolumns[$colname];
 471          if (!isset($column) || !$column->is_sortable()) {
 472              for ($i = 1; $i <= self::MAX_SORTS; $i++) {
 473                  $this->baseurl->remove_params('qbs' . $i);
 474              }
 475              throw new \moodle_exception('unknownsortcolumn', '', $link = $this->baseurl->out(), $colname);
 476          }
 477          // Validate the subsort, if present.
 478          if ($subsort) {
 479              $subsorts = $column->is_sortable();
 480              if (!is_array($subsorts) || !isset($subsorts[$subsort])) {
 481                  throw new \moodle_exception('unknownsortcolumn', '', $link = $this->baseurl->out(), $sort);
 482              }
 483          }
 484          return [$colname, $subsort];
 485      }
 486  
 487      /**
 488       * Initialise sort from parameters.
 489       */
 490      protected function init_sort_from_params(): void {
 491          $this->sort = [];
 492          for ($i = 1; $i <= self::MAX_SORTS; $i++) {
 493              if (!$sort = optional_param('qbs' . $i, '', PARAM_TEXT)) {
 494                  break;
 495              }
 496              // Work out the appropriate order.
 497              $order = 1;
 498              if ($sort[0] == '-') {
 499                  $order = -1;
 500                  $sort = substr($sort, 1);
 501                  if (!$sort) {
 502                      break;
 503                  }
 504              }
 505              // Deal with subsorts.
 506              list($colname) = $this->parse_subsort($sort);
 507              $this->get_column_type($colname);
 508              $this->sort[$sort] = $order;
 509          }
 510      }
 511  
 512      /**
 513       * Sort to parameters.
 514       *
 515       * @param array $sorts
 516       * @return array
 517       */
 518      protected function sort_to_params($sorts): array {
 519          $params = [];
 520          $i = 0;
 521          foreach ($sorts as $sort => $order) {
 522              $i += 1;
 523              if ($order < 0) {
 524                  $sort = '-' . $sort;
 525              }
 526              $params['qbs' . $i] = $sort;
 527          }
 528          return $params;
 529      }
 530  
 531      /**
 532       * Default sort for question data.
 533       * @return int[]
 534       */
 535      protected function default_sort(): array {
 536          $defaultsort = [];
 537          if (class_exists('\\qbank_viewquestiontype\\question_type_column')) {
 538              $sort = 'qbank_viewquestiontype\question_type_column';
 539          }
 540          $defaultsort[$sort] = 1;
 541          if (class_exists('\\qbank_viewquestionname\\question_name_idnumber_tags_column')) {
 542              $sort = 'qbank_viewquestionname\question_name_idnumber_tags_column';
 543          }
 544          $defaultsort[$sort . '-name'] = 1;
 545  
 546          return $defaultsort;
 547      }
 548  
 549      /**
 550       * Gets the primary sort order according to the default sort.
 551       *
 552       * @param string $sort a column or column_subsort name.
 553       * @return int the current sort order for this column -1, 0, 1
 554       */
 555      public function get_primary_sort_order($sort): int {
 556          $order = reset($this->sort);
 557          $primarysort = key($this->sort);
 558          if ($sort == $primarysort) {
 559              return $order;
 560          } else {
 561              return 0;
 562          }
 563      }
 564  
 565      /**
 566       * Get a URL to redisplay the page with a new sort for the question bank.
 567       *
 568       * @param string $sort the column, or column_subsort to sort on.
 569       * @param bool $newsortreverse whether to sort in reverse order.
 570       * @return string The new URL.
 571       */
 572      public function new_sort_url($sort, $newsortreverse): string {
 573          if ($newsortreverse) {
 574              $order = -1;
 575          } else {
 576              $order = 1;
 577          }
 578          // Tricky code to add the new sort at the start, removing it from where it was before, if it was present.
 579          $newsort = array_reverse($this->sort);
 580          if (isset($newsort[$sort])) {
 581              unset($newsort[$sort]);
 582          }
 583          $newsort[$sort] = $order;
 584          $newsort = array_reverse($newsort);
 585          if (count($newsort) > self::MAX_SORTS) {
 586              $newsort = array_slice($newsort, 0, self::MAX_SORTS, true);
 587          }
 588          return $this->baseurl->out(true, $this->sort_to_params($newsort));
 589      }
 590  
 591      /**
 592       * Create the SQL query to retrieve the indicated questions, based on
 593       * \core_question\bank\search\condition filters.
 594       */
 595      protected function build_query(): void {
 596          // Get the required tables and fields.
 597          $joins = [];
 598          $fields = ['qv.status', 'qc.id as categoryid', 'qv.version', 'qv.id as versionid', 'qbe.id as questionbankentryid'];
 599          if (!empty($this->requiredcolumns)) {
 600              foreach ($this->requiredcolumns as $column) {
 601                  $extrajoins = $column->get_extra_joins();
 602                  foreach ($extrajoins as $prefix => $join) {
 603                      if (isset($joins[$prefix]) && $joins[$prefix] != $join) {
 604                          throw new \coding_exception('Join ' . $join . ' conflicts with previous join ' . $joins[$prefix]);
 605                      }
 606                      $joins[$prefix] = $join;
 607                  }
 608                  $fields = array_merge($fields, $column->get_required_fields());
 609              }
 610          }
 611          $fields = array_unique($fields);
 612  
 613          // Build the order by clause.
 614          $sorts = [];
 615          foreach ($this->sort as $sort => $order) {
 616              list($colname, $subsort) = $this->parse_subsort($sort);
 617              $sorts[] = $this->requiredcolumns[$colname]->sort_expression($order < 0, $subsort);
 618          }
 619  
 620          // Build the where clause.
 621          $latestversion = 'qv.version = (SELECT MAX(v.version)
 622                                            FROM {question_versions} v
 623                                            JOIN {question_bank_entries} be
 624                                              ON be.id = v.questionbankentryid
 625                                           WHERE be.id = qbe.id)';
 626          $tests = ['q.parent = 0', $latestversion];
 627          $this->sqlparams = [];
 628          foreach ($this->searchconditions as $searchcondition) {
 629              if ($searchcondition->where()) {
 630                  $tests[] = '((' . $searchcondition->where() .'))';
 631              }
 632              if ($searchcondition->params()) {
 633                  $this->sqlparams = array_merge($this->sqlparams, $searchcondition->params());
 634              }
 635          }
 636          // Build the SQL.
 637          $sql = ' FROM {question} q ' . implode(' ', $joins);
 638          $sql .= ' WHERE ' . implode(' AND ', $tests);
 639          $this->countsql = 'SELECT count(1)' . $sql;
 640          $this->loadsql = 'SELECT ' . implode(', ', $fields) . $sql . ' ORDER BY ' . implode(', ', $sorts);
 641      }
 642  
 643      /**
 644       * Get the number of questions.
 645       * @return int
 646       */
 647      protected function get_question_count(): int {
 648          global $DB;
 649          return $DB->count_records_sql($this->countsql, $this->sqlparams);
 650      }
 651  
 652      /**
 653       * Load the questions we need to display.
 654       *
 655       * @param int $page page to display.
 656       * @param int $perpage number of questions per page.
 657       * @return \moodle_recordset questionid => data about each question.
 658       */
 659      protected function load_page_questions($page, $perpage): \moodle_recordset {
 660          global $DB;
 661          $questions = $DB->get_recordset_sql($this->loadsql, $this->sqlparams, $page * $perpage, $perpage);
 662          if (empty($questions)) {
 663              $questions->close();
 664              // No questions on this page. Reset to page 0.
 665              $questions = $DB->get_recordset_sql($this->loadsql, $this->sqlparams, 0, $perpage);
 666          }
 667          return $questions;
 668      }
 669  
 670      /**
 671       * Returns the base url.
 672       */
 673      public function base_url(): \moodle_url {
 674          return $this->baseurl;
 675      }
 676  
 677      /**
 678       * Get the URL for editing a question as a moodle url.
 679       *
 680       * @param int $questionid the question id.
 681       * @return \moodle_url the URL, HTML-escaped.
 682       */
 683      public function edit_question_moodle_url($questionid) {
 684          return new \moodle_url($this->editquestionurl, ['id' => $questionid]);
 685      }
 686  
 687      /**
 688       * Get the URL for editing a question as a HTML-escaped string.
 689       *
 690       * @param int $questionid the question id.
 691       * @return string the URL, HTML-escaped.
 692       */
 693      public function edit_question_url($questionid) {
 694          return $this->edit_question_moodle_url($questionid)->out();
 695      }
 696  
 697      /**
 698       * Get the URL for duplicating a question as a moodle url.
 699       *
 700       * @param int $questionid the question id.
 701       * @return \moodle_url the URL.
 702       */
 703      public function copy_question_moodle_url($questionid) {
 704          return new \moodle_url($this->editquestionurl, ['id' => $questionid, 'makecopy' => 1]);
 705      }
 706  
 707      /**
 708       * Get the URL for duplicating a given question.
 709       * @param int $questionid the question id.
 710       * @return string the URL, HTML-escaped.
 711       */
 712      public function copy_question_url($questionid) {
 713          return $this->copy_question_moodle_url($questionid)->out();
 714      }
 715  
 716      /**
 717       * Get the context we are displaying the question bank for.
 718       * @return \context context object.
 719       */
 720      public function get_most_specific_context(): \context {
 721          return $this->contexts->lowest();
 722      }
 723  
 724      /**
 725       * Get the URL to preview a question.
 726       * @param \stdClass $questiondata the data defining the question.
 727       * @return \moodle_url the URL.
 728       * @deprecated since Moodle 4.0
 729       * @see \qbank_previewquestion\helper::question_preview_url()
 730       * @todo Final deprecation on Moodle 4.4 MDL-72438
 731       */
 732      public function preview_question_url($questiondata) {
 733          debugging('Function preview_question_url() has been deprecated and moved to qbank_previewquestion plugin,
 734           please use qbank_previewquestion\helper::question_preview_url() instead.', DEBUG_DEVELOPER);
 735          return question_preview_url($questiondata->id, null, null, null, null,
 736                  $this->get_most_specific_context());
 737      }
 738  
 739      /**
 740       * Shows the question bank interface.
 741       *
 742       * The function also processes a number of actions:
 743       *
 744       * Actions affecting the question pool:
 745       * move           Moves a question to a different category
 746       * deleteselected Deletes the selected questions from the category
 747       * Other actions:
 748       * category      Chooses the category
 749       * params: $tabname question bank edit tab name, for permission checking
 750       * $pagevars current list of page variables
 751       *
 752       * @param string $tabname
 753       * @param array $pagevars
 754       */
 755      public function display($pagevars, $tabname): void {
 756  
 757          $page = $pagevars['qpage'];
 758          $perpage = $pagevars['qperpage'];
 759          $cat = $pagevars['cat'];
 760          $recurse = $pagevars['recurse'];
 761          $showhidden = $pagevars['showhidden'];
 762          $showquestiontext = $pagevars['qbshowtext'];
 763          $tagids = [];
 764          if (!empty($pagevars['qtagids'])) {
 765              $tagids = $pagevars['qtagids'];
 766          }
 767  
 768          echo \html_writer::start_div('questionbankwindow boxwidthwide boxaligncenter');
 769  
 770          $editcontexts = $this->contexts->having_one_edit_tab_cap($tabname);
 771  
 772          // Show the filters and search options.
 773          $this->wanted_filters($cat, $tagids, $showhidden, $recurse, $editcontexts, $showquestiontext);
 774  
 775          // Continues with list of questions.
 776          $this->display_question_list($this->baseurl, $cat, null, $page, $perpage,
 777                                          $this->contexts->having_cap('moodle/question:add'));
 778          echo \html_writer::end_div();
 779  
 780      }
 781  
 782      /**
 783       * The filters for the question bank.
 784       *
 785       * @param string $cat 'categoryid,contextid'
 786       * @param array $tagids current list of selected tags
 787       * @param bool $showhidden whether deleted questions should be displayed
 788       * @param int $recurse Whether to include subcategories
 789       * @param array $editcontexts parent contexts
 790       * @param bool $showquestiontext whether the text of each question should be shown in the list
 791       */
 792      public function wanted_filters($cat, $tagids, $showhidden, $recurse, $editcontexts, $showquestiontext): void {
 793          global $CFG;
 794          list(, $contextid) = explode(',', $cat);
 795          $catcontext = \context::instance_by_id($contextid);
 796          $thiscontext = $this->get_most_specific_context();
 797          // Category selection form.
 798          $this->display_question_bank_header();
 799  
 800          // Display tag filter if usetags setting is enabled/enablefilters is true.
 801          if ($this->enablefilters) {
 802              if (is_array($this->customfilterobjects)) {
 803                  foreach ($this->customfilterobjects as $filterobjects) {
 804                      $this->searchconditions[] = $filterobjects;
 805                  }
 806              } else {
 807                  if ($CFG->usetags) {
 808                      array_unshift($this->searchconditions,
 809                              new \core_question\bank\search\tag_condition([$catcontext, $thiscontext], $tagids));
 810                  }
 811  
 812                  array_unshift($this->searchconditions, new \core_question\bank\search\hidden_condition(!$showhidden));
 813                  array_unshift($this->searchconditions, new \core_question\bank\search\category_condition(
 814                          $cat, $recurse, $editcontexts, $this->baseurl, $this->course));
 815              }
 816          }
 817          $this->display_options_form($showquestiontext);
 818      }
 819  
 820      /**
 821       * Print the text if category id not available.
 822       */
 823      protected function print_choose_category_message(): void {
 824          echo \html_writer::start_tag('p', ['style' => "\"text-align:center;\""]);
 825          echo \html_writer::tag('b', get_string('selectcategoryabove', 'question'));
 826          echo \html_writer::end_tag('p');
 827      }
 828  
 829      /**
 830       * Gets current selected category.
 831       * @param string $categoryandcontext
 832       * @return false|mixed|\stdClass
 833       */
 834      protected function get_current_category($categoryandcontext) {
 835          global $DB, $OUTPUT;
 836          list($categoryid, $contextid) = explode(',', $categoryandcontext);
 837          if (!$categoryid) {
 838              $this->print_choose_category_message();
 839              return false;
 840          }
 841  
 842          if (!$category = $DB->get_record('question_categories',
 843                  ['id' => $categoryid, 'contextid' => $contextid])) {
 844              echo $OUTPUT->box_start('generalbox questionbank');
 845              echo $OUTPUT->notification('Category not found!');
 846              echo $OUTPUT->box_end();
 847              return false;
 848          }
 849  
 850          return $category;
 851      }
 852  
 853      /**
 854       * Display the form with options for which questions are displayed and how they are displayed.
 855       *
 856       * @param bool $showquestiontext Display the text of the question within the list.
 857       */
 858      protected function display_options_form($showquestiontext): void {
 859          global $PAGE;
 860  
 861          // The html will be refactored in the filter feature implementation.
 862          echo \html_writer::start_tag('form', ['method' => 'get',
 863                  'action' => new \moodle_url($this->baseurl), 'id' => 'displayoptions']);
 864          echo \html_writer::start_div();
 865  
 866          $excludes = ['recurse', 'showhidden', 'qbshowtext'];
 867          // If the URL contains any tags then we need to prevent them
 868          // being added to the form as hidden elements because the tags
 869          // are managed separately.
 870          if ($this->baseurl->param('qtagids[0]')) {
 871              $index = 0;
 872              while ($this->baseurl->param("qtagids[{$index}]")) {
 873                  $excludes[] = "qtagids[{$index}]";
 874                  $index++;
 875              }
 876          }
 877          echo \html_writer::input_hidden_params($this->baseurl, $excludes);
 878  
 879          $advancedsearch = [];
 880  
 881          foreach ($this->searchconditions as $searchcondition) {
 882              if ($searchcondition->display_options_adv()) {
 883                  $advancedsearch[] = $searchcondition;
 884              }
 885              echo $searchcondition->display_options();
 886          }
 887          $this->display_showtext_checkbox($showquestiontext);
 888          if (!empty($advancedsearch)) {
 889              $this->display_advanced_search_form($advancedsearch);
 890          }
 891  
 892          $go = \html_writer::empty_tag('input', ['type' => 'submit', 'value' => get_string('go')]);
 893          echo \html_writer::tag('noscript', \html_writer::div($go), ['class' => 'inline']);
 894          echo \html_writer::end_div();
 895          echo \html_writer::end_tag('form');
 896          $PAGE->requires->yui_module('moodle-question-searchform', 'M.question.searchform.init');
 897      }
 898  
 899      /**
 900       * Print the "advanced" UI elements for the form to select which questions. Hidden by default.
 901       *
 902       * @param array $advancedsearch
 903       */
 904      protected function display_advanced_search_form($advancedsearch): void {
 905          print_collapsible_region_start('', 'advancedsearch',
 906                  get_string('advancedsearchoptions', 'question'),
 907                  'question_bank_advanced_search');
 908          foreach ($advancedsearch as $searchcondition) {
 909              echo $searchcondition->display_options_adv();
 910          }
 911          print_collapsible_region_end();
 912      }
 913  
 914      /**
 915       * Display the checkbox UI for toggling the display of the question text in the list.
 916       * @param bool $showquestiontext the current or default value for whether to display the text.
 917       */
 918      protected function display_showtext_checkbox($showquestiontext): void {
 919          global $PAGE;
 920          $displaydata = [
 921                  'checked' => $showquestiontext
 922          ];
 923          if (class_exists('qbank_viewquestiontext\\question_text_row')) {
 924              if (\core\plugininfo\qbank::is_plugin_enabled('qbank_viewquestiontext')) {
 925                  echo $PAGE->get_renderer('core_question', 'bank')->render_showtext_checkbox($displaydata);
 926              }
 927          }
 928      }
 929  
 930      /**
 931       * Display the header element for the question bank.
 932       */
 933      protected function display_question_bank_header(): void {
 934          global $OUTPUT;
 935          echo $OUTPUT->heading(get_string('questionbank', 'question'), 2);
 936      }
 937  
 938      /**
 939       * Create a new question form.
 940       *
 941       * @param false|mixed|\stdClass $category
 942       * @param bool $canadd
 943       */
 944      protected function create_new_question_form($category, $canadd): void {
 945          if (\core\plugininfo\qbank::is_plugin_enabled('qbank_editquestion')) {
 946              echo editquestion_helper::create_new_question_button($category->id,
 947                      $this->requiredcolumns['edit_action_column']->editquestionurl->params(), $canadd);
 948          }
 949      }
 950  
 951      /**
 952       * Prints the table of questions in a category with interactions
 953       *
 954       * @param \moodle_url $pageurl     The URL to reload this page.
 955       * @param string     $categoryandcontext 'categoryID,contextID'.
 956       * @param int        $recurse     Whether to include subcategories.
 957       * @param int        $page        The number of the page to be displayed
 958       * @param int|null   $perpage     Number of questions to show per page
 959       * @param array      $addcontexts contexts where the user is allowed to add new questions.
 960       */
 961      protected function display_question_list($pageurl, $categoryandcontext, $recurse = 1, $page = 0,
 962                  $perpage = null, $addcontexts = []): void {
 963          global $OUTPUT;
 964          // This function can be moderately slow with large question counts and may time out.
 965          // We probably do not want to raise it to unlimited, so randomly picking 5 minutes.
 966          // Note: We do not call this in the loop because quiz ob_ captures this function (see raise() PHP doc).
 967          \core_php_time_limit::raise(300);
 968  
 969          $category = $this->get_current_category($categoryandcontext);
 970          $perpage = $perpage ?? $this->pagesize;
 971  
 972          list($categoryid, $contextid) = explode(',', $categoryandcontext);
 973          $catcontext = \context::instance_by_id($contextid);
 974  
 975          $canadd = has_capability('moodle/question:add', $catcontext);
 976  
 977          $this->create_new_question_form($category, $canadd);
 978  
 979          $this->build_query();
 980          $totalnumber = $this->get_question_count();
 981          if ($totalnumber == 0) {
 982              return;
 983          }
 984          $questionsrs = $this->load_page_questions($page, $perpage);
 985          $questions = [];
 986          foreach ($questionsrs as $question) {
 987              if (!empty($question->id)) {
 988                  $questions[$question->id] = $question;
 989              }
 990          }
 991          $questionsrs->close();
 992  
 993          // Bulk load any required statistics.
 994          $this->load_required_statistics($questions);
 995  
 996          // Bulk load any extra data that any column requires.
 997          foreach ($this->requiredcolumns as $name => $column) {
 998              $column->load_additional_data($questions);
 999          }
1000  
1001          $pageingurl = new \moodle_url($pageurl, $pageurl->params());
1002          $pagingbar = new \paging_bar($totalnumber, $page, $perpage, $pageingurl);
1003          $pagingbar->pagevar = 'qpage';
1004  
1005          $this->display_top_pagnation($OUTPUT->render($pagingbar));
1006  
1007          // This html will be refactored in the bulk actions implementation.
1008          echo \html_writer::start_tag('form', ['action' => $pageurl, 'method' => 'post', 'id' => 'questionsubmit']);
1009          echo \html_writer::start_tag('fieldset', ['class' => 'invisiblefieldset', 'style' => "display: block;"]);
1010          echo \html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()]);
1011          echo \html_writer::input_hidden_params($this->baseurl);
1012  
1013          $this->display_questions($questions);
1014  
1015          $this->display_bottom_pagination($OUTPUT->render($pagingbar), $totalnumber, $perpage, $pageurl);
1016  
1017          $this->display_bottom_controls($catcontext);
1018  
1019          echo \html_writer::end_tag('fieldset');
1020          echo \html_writer::end_tag('form');
1021      }
1022  
1023      /**
1024       * Work out the list of all the required statistics fields for this question bank view.
1025       *
1026       * This gathers all the required fields from all columns, so they can all be loaded at once.
1027       *
1028       * @return string[] the names of all the required fields for this question bank view.
1029       */
1030      protected function determine_required_statistics(): array {
1031          $requiredfields = [];
1032          foreach ($this->requiredcolumns as $column) {
1033              $requiredfields = array_merge($requiredfields, $column->get_required_statistics_fields());
1034          }
1035  
1036          return array_unique($requiredfields);
1037      }
1038  
1039      /**
1040       * Load the aggregate statistics that all the columns require.
1041       *
1042       * @param \stdClass[] $questions the questions that will be displayed indexed by question id.
1043       */
1044      protected function load_required_statistics(array $questions): void {
1045          $requiredstatistics = $this->determine_required_statistics();
1046          $this->loadedstatistics = statistics_bulk_loader::load_aggregate_statistics(
1047                  array_keys($questions), $requiredstatistics);
1048      }
1049  
1050      /**
1051       * Get the aggregated value of a particular statistic for a particular question.
1052       *
1053       * You can only get values for the questions on the current page of the question bank view,
1054       * and only if you declared the need for this statistic in the get_required_statistics_fields()
1055       * method of your question bank column.
1056       *
1057       * @param int $questionid the id of a question
1058       * @param string $fieldname the name of a statistics field, e.g. 'facility'.
1059       * @return float|null the average (across all users) of this statistic for this question.
1060       *      Null if the value is not available right now.
1061       */
1062      public function get_aggregate_statistic(int $questionid, string $fieldname): ?float {
1063          if (!array_key_exists($questionid, $this->loadedstatistics)) {
1064              throw new \coding_exception('Question ' . $questionid . ' is not on the current page of ' .
1065                      'this question bank view, so its statistics are not available.');
1066          }
1067  
1068          // Must be array_key_exists, not isset, because we care about null values.
1069          if (!array_key_exists($fieldname, $this->loadedstatistics[$questionid])) {
1070              throw new \coding_exception('Statistics field ' . $fieldname . ' was not requested by any ' .
1071                      'question bank column in this view, so it is not available.');
1072          }
1073  
1074          return $this->loadedstatistics[$questionid][$fieldname];
1075      }
1076  
1077      /**
1078       * Display the top pagination bar.
1079       *
1080       * @param object $pagination
1081       */
1082      protected function display_top_pagnation($pagination): void {
1083          global $PAGE;
1084          $displaydata = [
1085                  'pagination' => $pagination
1086          ];
1087          echo $PAGE->get_renderer('core_question', 'bank')->render_question_pagination($displaydata);
1088      }
1089  
1090      /**
1091       * Display bottom pagination bar.
1092       *
1093       * @param string $pagination
1094       * @param int $totalnumber
1095       * @param int $perpage
1096       * @param \moodle_url $pageurl
1097       */
1098      protected function display_bottom_pagination($pagination, $totalnumber, $perpage, $pageurl): void {
1099          global $PAGE;
1100          $displaydata = array (
1101                  'extraclasses' => 'pagingbottom',
1102                  'pagination' => $pagination,
1103                  'biggertotal' => true,
1104          );
1105          if ($totalnumber > $this->pagesize) {
1106              $displaydata['showall'] = true;
1107              if ($perpage == $this->pagesize) {
1108                  $url = new \moodle_url($pageurl, array_merge($pageurl->params(),
1109                          ['qpage' => 0, 'qperpage' => MAXIMUM_QUESTIONS_PER_PAGE]));
1110                  if ($totalnumber > MAXIMUM_QUESTIONS_PER_PAGE) {
1111                      $displaydata['totalnumber'] = MAXIMUM_QUESTIONS_PER_PAGE;
1112                  } else {
1113                      $displaydata['biggertotal'] = false;
1114                      $displaydata['totalnumber'] = $totalnumber;
1115                  }
1116              } else {
1117                  $url = new \moodle_url($pageurl, array_merge($pageurl->params(),
1118                          ['qperpage' => $this->pagesize]));
1119                  $displaydata['totalnumber'] = $this->pagesize;
1120              }
1121              $displaydata['showallurl'] = $url;
1122          }
1123          echo $PAGE->get_renderer('core_question', 'bank')->render_question_pagination($displaydata);
1124      }
1125  
1126      /**
1127       * Display the controls at the bottom of the list of questions.
1128       *
1129       * @param \context $catcontext The context of the category being displayed.
1130       */
1131      protected function display_bottom_controls(\context $catcontext): void {
1132          $caneditall = has_capability('moodle/question:editall', $catcontext);
1133          $canuseall = has_capability('moodle/question:useall', $catcontext);
1134          $canmoveall = has_capability('moodle/question:moveall', $catcontext);
1135          if ($caneditall || $canmoveall || $canuseall) {
1136              global $PAGE;
1137              $bulkactiondatas = [];
1138              $params = $this->base_url()->params();
1139              $params['returnurl'] = $this->base_url();
1140              foreach ($this->bulkactions as $key => $action) {
1141                  // Check capabilities.
1142                  $capcount = 0;
1143                  foreach ($action['capabilities'] as $capability) {
1144                      if (has_capability($capability, $catcontext)) {
1145                          $capcount ++;
1146                      }
1147                  }
1148                  // At least one cap need to be there.
1149                  if ($capcount === 0) {
1150                      unset($this->bulkactions[$key]);
1151                      continue;
1152                  }
1153                  $actiondata = new \stdClass();
1154                  $actiondata->actionname = $action['title'];
1155                  $actiondata->actionkey = $key;
1156                  $actiondata->actionurl = new \moodle_url($action['url'], $params);
1157                  $bulkactiondata[] = $actiondata;
1158  
1159                  $bulkactiondatas ['bulkactionitems'] = $bulkactiondata;
1160              }
1161              // We dont need to show this section if none of the plugins are enabled.
1162              if (!empty($bulkactiondatas)) {
1163                  echo $PAGE->get_renderer('core_question', 'bank')->render_bulk_actions_ui($bulkactiondatas);
1164              }
1165          }
1166      }
1167  
1168      /**
1169       * Display the questions.
1170       *
1171       * @param array $questions
1172       */
1173      protected function display_questions($questions): void {
1174          echo \html_writer::start_tag('div',
1175                  ['class' => 'categoryquestionscontainer', 'id' => 'questionscontainer']);
1176          $this->print_table($questions);
1177          echo \html_writer::end_tag('div');
1178      }
1179  
1180      /**
1181       * Prints the actual table with question.
1182       *
1183       * @param array $questions
1184       */
1185      protected function print_table($questions): void {
1186          // Start of the table.
1187          echo \html_writer::start_tag('table', ['id' => 'categoryquestions', 'class' => 'table-responsive']);
1188  
1189          // Prints the table header.
1190          echo \html_writer::start_tag('thead');
1191          echo \html_writer::start_tag('tr');
1192          $this->print_table_headers();
1193          echo \html_writer::end_tag('tr');
1194          echo \html_writer::end_tag('thead');
1195  
1196          // Prints the table row or content.
1197          echo \html_writer::start_tag('tbody');
1198          $rowcount = 0;
1199          foreach ($questions as $question) {
1200              $this->print_table_row($question, $rowcount);
1201              $rowcount += 1;
1202          }
1203          echo \html_writer::end_tag('tbody');
1204  
1205          // End of the table.
1206          echo \html_writer::end_tag('table');
1207      }
1208  
1209      /**
1210       * Start of the table html.
1211       *
1212       * @deprecated since Moodle 4.0
1213       * @see print_table()
1214       * @todo Final deprecation on Moodle 4.4 MDL-72438
1215       */
1216      protected function start_table() {
1217          debugging('Function start_table() is deprecated, please use print_table() instead.', DEBUG_DEVELOPER);
1218          echo '<table id="categoryquestions" class="table table-responsive">' . "\n";
1219          echo "<thead>\n";
1220          $this->print_table_headers();
1221          echo "</thead>\n";
1222          echo "<tbody>\n";
1223      }
1224  
1225      /**
1226       * End of the table html.
1227       *
1228       * @deprecated since Moodle 4.0
1229       * @see print_table()
1230       * @todo Final deprecation on Moodle 4.4 MDL-72438
1231       */
1232      protected function end_table() {
1233          debugging('Function end_table() is deprecated, please use print_table() instead.', DEBUG_DEVELOPER);
1234          echo "</tbody>\n";
1235          echo "</table>\n";
1236      }
1237  
1238      /**
1239       * Print table headers from child classes.
1240       */
1241      protected function print_table_headers(): void {
1242          foreach ($this->visiblecolumns as $column) {
1243              $column->display_header();
1244          }
1245      }
1246  
1247      /**
1248       * Gets the classes for the row.
1249       *
1250       * @param \stdClass $question
1251       * @param int $rowcount
1252       * @return array
1253       */
1254      protected function get_row_classes($question, $rowcount): array {
1255          $classes = [];
1256          if ($question->status === question_version_status::QUESTION_STATUS_HIDDEN) {
1257              $classes[] = 'dimmed_text';
1258          }
1259          if ($question->id == $this->lastchangedid) {
1260              $classes[] = 'highlight text-dark';
1261          }
1262          $classes[] = 'r' . ($rowcount % 2);
1263          return $classes;
1264      }
1265  
1266      /**
1267       * Prints the table row from child classes.
1268       *
1269       * @param \stdClass $question
1270       * @param int $rowcount
1271       */
1272      protected function print_table_row($question, $rowcount): void {
1273          $rowclasses = implode(' ', $this->get_row_classes($question, $rowcount));
1274          $attributes = [];
1275          if ($rowclasses) {
1276              $attributes['class'] = $rowclasses;
1277          }
1278          echo \html_writer::start_tag('tr', $attributes);
1279          foreach ($this->visiblecolumns as $column) {
1280              $column->display($question, $rowclasses);
1281          }
1282          echo \html_writer::end_tag('tr');
1283          foreach ($this->extrarows as $row) {
1284              $row->display($question, $rowclasses);
1285          }
1286      }
1287  
1288      /**
1289       * Process actions for the selected action.
1290       * @deprecated since Moodle 4.0
1291       * @todo Final deprecation on Moodle 4.4 MDL-72438
1292       */
1293      public function process_actions(): void {
1294          debugging('Function process_actions() is deprecated and its code has been completely deleted.
1295           Please, remove the call from your code and check core_question\local\bank\bulk_action_base
1296            to learn more about bulk actions in qbank.', DEBUG_DEVELOPER);
1297          // Associated code is deleted to make sure any incorrect call doesnt not cause any data loss.
1298      }
1299  
1300      /**
1301       * Process actions with ui.
1302       * @return bool
1303       * @deprecated since Moodle 4.0
1304       * @todo Final deprecation on Moodle 4.4 MDL-72438
1305       */
1306      public function process_actions_needing_ui(): bool {
1307          debugging('Function process_actions_needing_ui() is deprecated and its code has been completely deleted.
1308           Please, remove the call from your code and check core_question\local\bank\bulk_action_base
1309            to learn more about bulk actions in qbank.', DEBUG_DEVELOPER);
1310          // Associated code is deleted to make sure any incorrect call doesnt not cause any data loss.
1311          return false;
1312      }
1313  
1314      /**
1315       * Add another search control to this view.
1316       * @param condition $searchcondition the condition to add.
1317       */
1318      public function add_searchcondition($searchcondition): void {
1319          $this->searchconditions[] = $searchcondition;
1320      }
1321  
1322      /**
1323       * Gets visible columns.
1324       * @return array Visible columns.
1325       */
1326      public function get_visiblecolumns(): array {
1327          return $this->visiblecolumns;
1328      }
1329  
1330      /**
1331       * Is this view showing separate versions of a question?
1332       *
1333       * @return bool
1334       */
1335      public function is_listing_specific_versions(): bool {
1336          return false;
1337      }
1338  }