Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

Differences Between: [Versions 310 and 400] [Versions 311 and 400] [Versions 39 and 400] [Versions 400 and 401] [Versions 400 and 402] [Versions 400 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   * Defines the custom question bank view used on the Edit quiz page.
  19   *
  20   * @package   mod_quiz
  21   * @category  question
  22   * @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com}
  23   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  namespace mod_quiz\question\bank;
  27  
  28  use core_question\local\bank\question_version_status;
  29  use mod_quiz\question\bank\filter\custom_category_condition;
  30  
  31  /**
  32   * Subclass to customise the view of the question bank for the quiz editing screen.
  33   *
  34   * @copyright  2009 Tim Hunt
  35   * @author     2021 Safat Shahin <safatshahin@catalyst-au.net>
  36   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  37   */
  38  class custom_view extends \core_question\local\bank\view {
  39  
  40      /** @var bool $quizhasattempts whether the quiz this is used by has been attemptd. */
  41      protected $quizhasattempts = false;
  42  
  43      /** @var \stdClass $quiz the quiz settings. */
  44      protected $quiz = false;
  45  
  46      /** @var int The maximum displayed length of the category info. */
  47      const MAX_TEXT_LENGTH = 200;
  48  
  49      /**
  50       * Constructor.
  51       * @param \core_question\local\bank\question_edit_contexts $contexts
  52       * @param \moodle_url $pageurl
  53       * @param \stdClass $course course settings
  54       * @param \stdClass $cm activity settings.
  55       * @param \stdClass $quiz quiz settings.
  56       */
  57      public function __construct($contexts, $pageurl, $course, $cm, $quiz) {
  58          parent::__construct($contexts, $pageurl, $course, $cm);
  59          $this->quiz = $quiz;
  60      }
  61  
  62      protected function get_question_bank_plugins(): array {
  63          $questionbankclasscolumns = [];
  64          $corequestionbankcolumns = [
  65              'add_action_column',
  66              'checkbox_column',
  67              'question_type_column',
  68              'question_name_text_column',
  69              'preview_action_column'
  70          ];
  71  
  72          if (question_get_display_preference('qbshowtext', 0, PARAM_INT, new \moodle_url(''))) {
  73              $corequestionbankcolumns[] = 'question_text_row';
  74          }
  75  
  76          foreach ($corequestionbankcolumns as $fullname) {
  77              $shortname = $fullname;
  78              if (class_exists('mod_quiz\\question\\bank\\' . $fullname)) {
  79                  $fullname = 'mod_quiz\\question\\bank\\' . $fullname;
  80                  $questionbankclasscolumns[$shortname] = new $fullname($this);
  81              } else if (class_exists('core_question\\local\\bank\\' . $fullname)) {
  82                  $fullname = 'core_question\\local\\bank\\' . $fullname;
  83                  $questionbankclasscolumns[$shortname] = new $fullname($this);
  84              } else {
  85                  $questionbankclasscolumns[$shortname] = '';
  86              }
  87          }
  88          $plugins = \core_component::get_plugin_list_with_class('qbank', 'plugin_feature', 'plugin_feature.php');
  89          foreach ($plugins as $componentname => $plugin) {
  90              $pluginentrypointobject = new $plugin();
  91              $plugincolumnobjects = $pluginentrypointobject->get_question_columns($this);
  92              // Don't need the plugins without column objects.
  93              if (empty($plugincolumnobjects)) {
  94                  unset($plugins[$componentname]);
  95                  continue;
  96              }
  97              foreach ($plugincolumnobjects as $columnobject) {
  98                  $columnname = $columnobject->get_column_name();
  99                  foreach ($corequestionbankcolumns as $key => $corequestionbankcolumn) {
 100                      if (!\core\plugininfo\qbank::is_plugin_enabled($componentname)) {
 101                          unset($questionbankclasscolumns[$columnname]);
 102                          continue;
 103                      }
 104                      // Check if it has custom preference selector to view/hide.
 105                      if ($columnobject->has_preference() && !$columnobject->get_preference()) {
 106                          continue;
 107                      }
 108                      if ($corequestionbankcolumn === $columnname) {
 109                          $questionbankclasscolumns[$columnname] = $columnobject;
 110                      }
 111                  }
 112              }
 113          }
 114  
 115          // Mitigate the error in case of any regression.
 116          foreach ($questionbankclasscolumns as $shortname => $questionbankclasscolumn) {
 117              if (empty($questionbankclasscolumn)) {
 118                  unset($questionbankclasscolumns[$shortname]);
 119              }
 120          }
 121  
 122          return $questionbankclasscolumns;
 123      }
 124  
 125      protected function heading_column(): string {
 126          return 'mod_quiz\\question\\bank\\question_name_text_column';
 127      }
 128  
 129      protected function default_sort(): array {
 130          // Using the extended class for quiz specific sort.
 131          return [
 132              'qbank_viewquestiontype\\question_type_column' => 1,
 133              'mod_quiz\\question\\bank\\question_name_text_column' => 1,
 134          ];
 135      }
 136  
 137      /**
 138       * Let the question bank display know whether the quiz has been attempted,
 139       * hence whether some bits of UI, like the add this question to the quiz icon,
 140       * should be displayed.
 141       *
 142       * @param bool $quizhasattempts whether the quiz has attempts.
 143       */
 144      public function set_quiz_has_attempts($quizhasattempts): void {
 145          $this->quizhasattempts = $quizhasattempts;
 146          if ($quizhasattempts && isset($this->visiblecolumns['addtoquizaction'])) {
 147              unset($this->visiblecolumns['addtoquizaction']);
 148          }
 149      }
 150  
 151      /**
 152       * Question preview url.
 153       *
 154       * @param \stdClass $question
 155       * @return \moodle_url
 156       */
 157      public function preview_question_url($question) {
 158          return quiz_question_preview_url($this->quiz, $question);
 159      }
 160  
 161      /**
 162       * URL of add to quiz.
 163       *
 164       * @param $questionid
 165       * @return \moodle_url
 166       */
 167      public function add_to_quiz_url($questionid) {
 168          $params = $this->baseurl->params();
 169          $params['addquestion'] = $questionid;
 170          $params['sesskey'] = sesskey();
 171          return new \moodle_url('/mod/quiz/edit.php', $params);
 172      }
 173  
 174      /**
 175       * Renders the html question bank (same as display, but returns the result).
 176       *
 177       * Note that you can only output this rendered result once per page, as
 178       * it contains IDs which must be unique.
 179       *
 180       * @param array $pagevars
 181       * @param string $tabname
 182       * @return string HTML code for the form
 183       */
 184      public function render($pagevars, $tabname): string {
 185          ob_start();
 186          $this->display($pagevars, $tabname);
 187          $out = ob_get_contents();
 188          ob_end_clean();
 189          return $out;
 190      }
 191  
 192      protected function display_bottom_controls(\context $catcontext): void {
 193          $cmoptions = new \stdClass();
 194          $cmoptions->hasattempts = !empty($this->quizhasattempts);
 195  
 196          $canuseall = has_capability('moodle/question:useall', $catcontext);
 197  
 198          echo \html_writer::start_tag('div', ['class' => 'pt-2']);
 199          if ($canuseall) {
 200              // Add selected questions to the quiz.
 201              $params = array(
 202                  'type' => 'submit',
 203                  'name' => 'add',
 204                  'class' => 'btn btn-primary',
 205                  'value' => get_string('addselectedquestionstoquiz', 'quiz'),
 206                  'data-action' => 'toggle',
 207                  'data-togglegroup' => 'qbank',
 208                  'data-toggle' => 'action',
 209                  'disabled' => true,
 210              );
 211              echo \html_writer::empty_tag('input', $params);
 212          }
 213          echo \html_writer::end_tag('div');
 214      }
 215  
 216      protected function create_new_question_form($category, $canadd): void {
 217          // Don't display this.
 218      }
 219  
 220      /**
 221       * Override the base implementation in \core_question\local\bank\view
 222       * because we don't want to print the headers in the fragment
 223       * for the modal.
 224       */
 225      protected function display_question_bank_header(): void {
 226      }
 227  
 228      /**
 229       * Override the base implementation in \core_question\bank\view
 230       * because we don't want it to read from the $_POST global variables
 231       * for the sort parameters since they are not present in a fragment.
 232       *
 233       * Unfortunately the best we can do is to look at the URL for
 234       * those parameters (only marginally better really).
 235       */
 236      protected function init_sort_from_params(): void {
 237          $this->sort = [];
 238          for ($i = 1; $i <= self::MAX_SORTS; $i++) {
 239              if (!$sort = $this->baseurl->param('qbs' . $i)) {
 240                  break;
 241              }
 242              // Work out the appropriate order.
 243              $order = 1;
 244              if ($sort[0] == '-') {
 245                  $order = -1;
 246                  $sort = substr($sort, 1);
 247                  if (!$sort) {
 248                      break;
 249                  }
 250              }
 251              // Deal with subsorts.
 252              list($colname) = $this->parse_subsort($sort);
 253              $this->get_column_type($colname);
 254              $this->sort[$sort] = $order;
 255          }
 256      }
 257  
 258      protected function build_query(): void {
 259          // Get the required tables and fields.
 260          $joins = [];
 261          $fields = ['qv.status', 'qc.id as categoryid', 'qv.version', 'qv.id as versionid', 'qbe.id as questionbankentryid'];
 262          if (!empty($this->requiredcolumns)) {
 263              foreach ($this->requiredcolumns as $column) {
 264                  $extrajoins = $column->get_extra_joins();
 265                  foreach ($extrajoins as $prefix => $join) {
 266                      if (isset($joins[$prefix]) && $joins[$prefix] != $join) {
 267                          throw new \coding_exception('Join ' . $join . ' conflicts with previous join ' . $joins[$prefix]);
 268                      }
 269                      $joins[$prefix] = $join;
 270                  }
 271                  $fields = array_merge($fields, $column->get_required_fields());
 272              }
 273          }
 274          $fields = array_unique($fields);
 275  
 276          // Build the order by clause.
 277          $sorts = [];
 278          foreach ($this->sort as $sort => $order) {
 279              list($colname, $subsort) = $this->parse_subsort($sort);
 280              $sorts[] = $this->requiredcolumns[$colname]->sort_expression($order < 0, $subsort);
 281          }
 282  
 283          // Build the where clause.
 284          $latestversion = 'qv.version = (SELECT MAX(v.version)
 285                                            FROM {question_versions} v
 286                                            JOIN {question_bank_entries} be
 287                                              ON be.id = v.questionbankentryid
 288                                           WHERE be.id = qbe.id)';
 289          $readyonly = "qv.status = '" . question_version_status::QUESTION_STATUS_READY . "' ";
 290          $tests = ['q.parent = 0', $latestversion, $readyonly];
 291          $this->sqlparams = [];
 292          foreach ($this->searchconditions as $searchcondition) {
 293              if ($searchcondition->where()) {
 294                  $tests[] = '((' . $searchcondition->where() .'))';
 295              }
 296              if ($searchcondition->params()) {
 297                  $this->sqlparams = array_merge($this->sqlparams, $searchcondition->params());
 298              }
 299          }
 300          // Build the SQL.
 301          $sql = ' FROM {question} q ' . implode(' ', $joins);
 302          $sql .= ' WHERE ' . implode(' AND ', $tests);
 303          $this->countsql = 'SELECT count(1)' . $sql;
 304          $this->loadsql = 'SELECT ' . implode(', ', $fields) . $sql . ' ORDER BY ' . implode(', ', $sorts);
 305      }
 306  
 307      public function wanted_filters($cat, $tagids, $showhidden, $recurse, $editcontexts, $showquestiontext): void {
 308          global $CFG;
 309          list(, $contextid) = explode(',', $cat);
 310          $catcontext = \context::instance_by_id($contextid);
 311          $thiscontext = $this->get_most_specific_context();
 312          // Category selection form.
 313          $this->display_question_bank_header();
 314  
 315          // Display tag filter if usetags setting is enabled/enablefilters is true.
 316          if ($this->enablefilters) {
 317              if (is_array($this->customfilterobjects)) {
 318                  foreach ($this->customfilterobjects as $filterobjects) {
 319                      $this->searchconditions[] = $filterobjects;
 320                  }
 321              } else {
 322                  if ($CFG->usetags) {
 323                      array_unshift($this->searchconditions,
 324                          new \core_question\bank\search\tag_condition([$catcontext, $thiscontext], $tagids));
 325                  }
 326  
 327                  array_unshift($this->searchconditions, new \core_question\bank\search\hidden_condition(!$showhidden));
 328                  array_unshift($this->searchconditions, new custom_category_condition(
 329                      $cat, $recurse, $editcontexts, $this->baseurl, $this->course));
 330              }
 331          }
 332          $this->display_options_form($showquestiontext);
 333      }
 334  }