MDL-40457 Question Bank: Allow plugins to add columns to question bank view
authorRay Morris <Ray.Morris@teex.tamu.edu>
Tue, 11 Mar 2014 14:16:33 +0000 (09:16 -0500)
committerRay Morris <Ray.Morris@teex.tamu.edu>
Mon, 30 Jun 2014 02:28:52 +0000 (21:28 -0500)
Allows plugins to add columns to the question bank view by extending core_question\bank\column_base
Columns to display are set in $CFG->questionbankcolumns. Columns are namespaced and autoloaded to support this.

18 files changed:
mod/quiz/editlib.php
question/classes/bank/action_column_base.php [new file with mode: 0644]
question/classes/bank/checkbox_column.php [new file with mode: 0644]
question/classes/bank/column_base.php [new file with mode: 0644]
question/classes/bank/copy_action_column.php [new file with mode: 0644]
question/classes/bank/creator_name_column.php [new file with mode: 0644]
question/classes/bank/delete_action_column.php [new file with mode: 0644]
question/classes/bank/edit_action_column.php [new file with mode: 0644]
question/classes/bank/modifier_name_column.php [new file with mode: 0644]
question/classes/bank/preview_action_column.php [new file with mode: 0644]
question/classes/bank/question_name_column.php [new file with mode: 0644]
question/classes/bank/question_text_row.php [new file with mode: 0644]
question/classes/bank/question_type_column.php [new file with mode: 0644]
question/classes/bank/row_base.php [new file with mode: 0644]
question/classes/bank/view.php [new file with mode: 0644]
question/edit.php
question/editlib.php
question/upgrade.txt

index 47a1086..e98e594 100644 (file)
@@ -1103,7 +1103,7 @@ class question_bank_question_name_text_column extends question_bank_question_nam
  * @copyright  2009 Tim Hunt
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class quiz_question_bank_view extends question_bank_view {
+class quiz_question_bank_view extends core_question\bank\view {
     protected $quizhasattempts = false;
     /** @var object the quiz settings. */
     protected $quiz = false;
@@ -1123,16 +1123,27 @@ class quiz_question_bank_view extends question_bank_view {
         $this->quiz = $quiz;
     }
 
-    protected function known_field_types() {
-        $types = parent::known_field_types();
-        $types[] = new question_bank_add_to_quiz_action_column($this);
-        $types[] = new question_bank_question_name_text_column($this);
-        return $types;
-    }
-
     protected function wanted_columns() {
-        return array('addtoquizaction', 'checkbox', 'qtype', 'questionnametext',
-                'editaction', 'copyaction', 'previewaction');
+        global $CFG;
+
+        if (empty($CFG->quizquestionbankcolumns)) {
+            $quizquestionbankcolumns = array('add_to_quiz_action_column', 'checkbox_column', 'question_type_column',
+                    'question_name_column', 'edit_action_column', 'preview_action_column');
+        } else {
+             $quizquestionbankcolumns = explode(',', $CFG->quizquestionbankcolumns);
+        }
+
+        foreach ($quizquestionbankcolumns as $fullname) {
+            if (! class_exists($fullname)) {
+                if (class_exists('question_bank_' . $fullname)) {
+                    $fullname = 'question_bank_' . $fullname;
+                } else {
+                    throw new coding_exception("No such class exists: $fullname");
+                }
+            }
+            $this->requiredcolumns[$fullname] = new $fullname($this);
+        }
+        return $this->requiredcolumns;
     }
 
     /**
@@ -1145,9 +1156,7 @@ class quiz_question_bank_view extends question_bank_view {
     }
 
     protected function default_sort() {
-        $this->requiredcolumns['qtype'] = $this->knowncolumntypes['qtype'];
-        $this->requiredcolumns['questionnametext'] = $this->knowncolumntypes['questionnametext'];
-        return array('qtype' => 1, 'questionnametext' => 1);
+        return array('question_bank_question_type_column' => 1, 'question_bank_question_name_column' => 1);
     }
 
     /**
diff --git a/question/classes/bank/action_column_base.php b/question/classes/bank/action_column_base.php
new file mode 100644 (file)
index 0000000..6d2cd2f
--- /dev/null
@@ -0,0 +1,46 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+namespace core_question\bank;
+
+/**
+ * A base class for actions that are an icon that lets you manipulate the question in some way.
+ *
+ * @copyright  2009 Tim Hunt
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+abstract class action_column_base extends column_base {
+
+    protected function get_title() {
+        return '&#160;';
+    }
+
+    public function get_extra_classes() {
+        return array('iconcol');
+    }
+
+    protected function print_icon($icon, $title, $url) {
+        global $OUTPUT;
+        echo '<a title="' . $title . '" href="' . $url . '">
+                <img src="' . $OUTPUT->pix_url($icon) . '" class="iconsmall" alt="' . $title . '" /></a>';
+    }
+
+    public function get_required_fields() {
+        // Createdby is required for permission checks.
+        return array('q.id', 'q.createdby');
+    }
+}
diff --git a/question/classes/bank/checkbox_column.php b/question/classes/bank/checkbox_column.php
new file mode 100644 (file)
index 0000000..9ba0ac3
--- /dev/null
@@ -0,0 +1,60 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+namespace core_question\bank;
+
+/**
+ * A column with a checkbox for each question with name q{questionid}.
+ *
+ * @copyright  2009 Tim Hunt
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class checkbox_column extends column_base {
+    protected $strselect;
+    protected $firstrow = true;
+
+    public function init() {
+        $this->strselect = get_string('select');
+    }
+
+    public function get_name() {
+        return 'checkbox';
+    }
+
+    protected function get_title() {
+        return '<input type="checkbox" disabled="disabled" id="qbheadercheckbox" />';
+    }
+
+    protected function get_title_tip() {
+        return get_string('selectquestionsforbulk', 'question');
+    }
+
+    protected function display_content($question, $rowclasses) {
+        global $PAGE;
+        echo '<input title="' . $this->strselect . '" type="checkbox" name="q' .
+                $question->id . '" id="checkq' . $question->id . '" value="1"/>';
+        if ($this->firstrow) {
+            $PAGE->requires->strings_for_js(array('selectall', 'deselectall'), 'moodle');
+            $PAGE->requires->yui_module('moodle-question-qbankmanager', 'M.question.qbankmanager.init',
+                    array('checkq' . $question->id));
+            $this->firstrow = false;
+        }
+    }
+
+    public function get_required_fields() {
+        return array('q.id');
+    }
+}
diff --git a/question/classes/bank/column_base.php b/question/classes/bank/column_base.php
new file mode 100644 (file)
index 0000000..b300ec7
--- /dev/null
@@ -0,0 +1,312 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ *
+ * @package    moodlecore
+ * @subpackage questionbank
+ * @copyright  1999 onwards Martin Dougiamas and others {@link http://moodle.com}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+namespace core_question\bank;
+
+/**
+ * Base class for representing a column in a {@link question_bank_view}.
+ *
+ * @copyright  2009 Tim Hunt
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+abstract class column_base {
+    /**
+     * @var question_bank_view
+     */
+    protected $qbank;
+
+    /** @var bool determine whether the column is td or th. */
+    protected $isheading = false;
+
+    /**
+     * Constructor.
+     * @param $qbank the question_bank_view we are helping to render.
+     */
+    public function __construct(view $qbank) {
+        $this->qbank = $qbank;
+        $this->init();
+    }
+
+    /**
+     * A chance for subclasses to initialise themselves, for example to load lang strings,
+     * without having to override the constructor.
+     */
+    protected function init() {
+    }
+
+    /**
+     * Set the column as heading
+     */
+    public function set_as_heading() {
+        $this->isheading = true;
+    }
+
+    public function is_extra_row() {
+        return false;
+    }
+
+    /**
+     * Output the column header cell.
+     */
+    public function display_header() {
+        echo '<th class="header ' . $this->get_classes() . '" scope="col">';
+        $sortable = $this->is_sortable();
+        $name = get_class($this);
+        $title = $this->get_title();
+        $tip = $this->get_title_tip();
+        if (is_array($sortable)) {
+            if ($title) {
+                echo '<div class="title">' . $title . '</div>';
+            }
+            $links = array();
+            foreach ($sortable as $subsort => $details) {
+                $links[] = $this->make_sort_link($name . '-' . $subsort,
+                        $details['title'], '', !empty($details['reverse']));
+            }
+            echo '<div class="sorters">' . implode(' / ', $links) . '</div>';
+        } else if ($sortable) {
+            echo $this->make_sort_link($name, $title, $tip);
+        } else {
+            if ($tip) {
+                echo '<span title="' . $tip . '">';
+            }
+            echo $title;
+            if ($tip) {
+                echo '</span>';
+            }
+        }
+        echo "</th>\n";
+    }
+
+    /**
+     * Title for this column. Not used if is_sortable returns an array.
+     * @param object $question the row from the $question table, augmented with extra information.
+     * @param string $rowclasses CSS class names that should be applied to this row of output.
+     */
+    protected abstract function get_title();
+
+    /**
+     * @return string a fuller version of the name. Use this when get_title() returns
+     * something very short, and you want a longer version as a tool tip.
+     */
+    protected function get_title_tip() {
+        return '';
+    }
+
+    /**
+     * Get a link that changes the sort order, and indicates the current sort state.
+     * @param $name internal name used for this type of sorting.
+     * @param $currentsort the current sort order -1, 0, 1 for descending, none, ascending.
+     * @param $title the link text.
+     * @param $defaultreverse whether the default sort order for this column is descending, rather than ascending.
+     * @return string HTML fragment.
+     */
+    protected function make_sort_link($sort, $title, $tip, $defaultreverse = false) {
+        $currentsort = $this->qbank->get_primary_sort_order($sort);
+        $newsortreverse = $defaultreverse;
+        if ($currentsort) {
+            $newsortreverse = $currentsort > 0;
+        }
+        if (!$tip) {
+            $tip = $title;
+        }
+        if ($newsortreverse) {
+            $tip = get_string('sortbyxreverse', '', $tip);
+        } else {
+            $tip = get_string('sortbyx', '', $tip);
+        }
+        $link = '<a href="' . $this->qbank->new_sort_url($sort, $newsortreverse) . '" title="' . $tip . '">';
+        $link .= $title;
+        if ($currentsort) {
+            $link .= $this->get_sort_icon($currentsort < 0);
+        }
+        $link .= '</a>';
+        return $link;
+    }
+
+    /**
+     * Get an icon representing the corrent sort state.
+     * @param $reverse sort is descending, not ascending.
+     * @return string HTML image tag.
+     */
+    protected function get_sort_icon($reverse) {
+        global $OUTPUT;
+        if ($reverse) {
+            return $OUTPUT->pix_icon('t/sort_desc', get_string('desc'), '', array('class' => 'iconsort'));
+        } else {
+            return $OUTPUT->pix_icon('t/sort_asc', get_string('asc'), '', array('class' => 'iconsort'));
+        }
+    }
+
+    /**
+     * Output this column.
+     * @param object $question the row from the $question table, augmented with extra information.
+     * @param string $rowclasses CSS class names that should be applied to this row of output.
+     */
+    public function display($question, $rowclasses) {
+        $this->display_start($question, $rowclasses);
+        $this->display_content($question, $rowclasses);
+        $this->display_end($question, $rowclasses);
+    }
+
+    /**
+     * Output the opening column tag.  If it is set as heading, it will use <th> tag instead of <td>
+     *
+     * @param stdClass $question
+     * @param array $rowclasses
+     */
+    protected function display_start($question, $rowclasses) {
+        $tag = 'td';
+        $attr = array('class' => $this->get_classes());
+        if ($this->isheading) {
+            $tag = 'th';
+            $attr['scope'] = 'row';
+        }
+        echo \html_writer::start_tag($tag, $attr);
+    }
+
+    /**
+     * @return string the CSS classes to apply to every cell in this column.
+     */
+    protected function get_classes() {
+        $classes = $this->get_extra_classes();
+        $classes[] = $this->get_name();
+        return implode(' ', $classes);
+    }
+
+    /**
+     * @param object $question the row from the $question table, augmented with extra information.
+     * @return string internal name for this column. Used as a CSS class name,
+     *     and to store information about the current sort. Must match PARAM_ALPHA.
+     */
+    public abstract function get_name();
+
+    /**
+     * @return array any extra class names you would like applied to every cell in this column.
+     */
+    public function get_extra_classes() {
+        return array();
+    }
+
+    /**
+     * Output the contents of this column.
+     * @param object $question the row from the $question table, augmented with extra information.
+     * @param string $rowclasses CSS class names that should be applied to this row of output.
+     */
+    protected abstract function display_content($question, $rowclasses);
+
+    /**
+     * Output the closing column tag
+     *
+     * @param object $question
+     * @param string $rowclasses
+     */
+    protected function display_end($question, $rowclasses) {
+        $tag = 'td';
+        if ($this->isheading) {
+            $tag = 'th';
+        }
+        echo \html_writer::end_tag($tag);
+    }
+
+    /**
+     * Return an array 'table_alias' => 'JOIN clause' to bring in any data that
+     * this column required.
+     *
+     * The return values for all the columns will be checked. It is OK if two
+     * columns join in the same table with the same alias and identical JOIN clauses.
+     * If to columns try to use the same alias with different joins, you get an error.
+     * The only table included by default is the question table, which is aliased to 'q'.
+     *
+     * It is importnat that your join simply adds additional data (or NULLs) to the
+     * existing rows of the query. It must not cause additional rows.
+     *
+     * @return array 'table_alias' => 'JOIN clause'
+     */
+    public function get_extra_joins() {
+        return array();
+    }
+
+    /**
+     * @return array fields required. use table alias 'q' for the question table, or one of the
+     * ones from get_extra_joins. Every field requested must specify a table prefix.
+     */
+    public function get_required_fields() {
+        return array();
+    }
+
+    /**
+     * Can this column be sorted on? You can return either:
+     *  + false for no (the default),
+     *  + a field name, if sorting this column corresponds to sorting on that datbase field.
+     *  + an array of subnames to sort on as follows
+     *  return array(
+     *      'firstname' => array('field' => 'uc.firstname', 'title' => get_string('firstname')),
+     *      'lastname' => array('field' => 'uc.lastname', 'field' => get_string('lastname')),
+     *  );
+     * As well as field, and field, you can also add 'revers' => 1 if you want the default sort
+     * order to be DESC.
+     * @return mixed as above.
+     */
+    public function is_sortable() {
+        return false;
+    }
+
+    /**
+     * Helper method for building sort clauses.
+     * @param bool $reverse whether the normal direction should be reversed.
+     * @param string $normaldir 'ASC' or 'DESC'
+     * @return string 'ASC' or 'DESC'
+     */
+    protected function sortorder($reverse) {
+        if ($reverse) {
+            return ' DESC';
+        } else {
+            return ' ASC';
+        }
+    }
+
+    /**
+     * @param $reverse Whether to sort in the reverse of the default sort order.
+     * @param $subsort if is_sortable returns an array of subnames, then this will be
+     *      one of those. Otherwise will be empty.
+     * @return string some SQL to go in the order by clause.
+     */
+    public function sort_expression($reverse, $subsort) {
+        $sortable = $this->is_sortable();
+        if (is_array($sortable)) {
+            if (array_key_exists($subsort, $sortable)) {
+                return $sortable[$subsort]['field'] . $this->sortorder($reverse, !empty($sortable[$subsort]['reverse']));
+            } else {
+                throw new coding_exception('Unexpected $subsort type: ' . $subsort);
+            }
+        } else if ($sortable) {
+            return $sortable . $this->sortorder($reverse);
+        } else {
+            throw new coding_exception('sort_expression called on a non-sortable column.');
+        }
+    }
+}
diff --git a/question/classes/bank/copy_action_column.php b/question/classes/bank/copy_action_column.php
new file mode 100644 (file)
index 0000000..9d0d890
--- /dev/null
@@ -0,0 +1,48 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+namespace core_question\bank;
+
+/**
+ * Question bank column for the duplicate action icon.
+ *
+ * @copyright  2013 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+class copy_action_column extends action_column_base {
+    /** @var string avoids repeated calls to get_string('duplicate'). */
+    protected $strcopy;
+
+    public function init() {
+        parent::init();
+        $this->strcopy = get_string('duplicate');
+    }
+
+    public function get_name() {
+        return 'copyaction';
+    }
+
+    protected function display_content($question, $rowclasses) {
+        // To copy a question, you need permission to add a question in the same
+        // category as the existing question, and ability to access the details of
+        // the question being copied.
+        if (question_has_capability_on($question, 'add') &&
+                (question_has_capability_on($question, 'edit') || question_has_capability_on($question, 'view'))) {
+            $this->print_icon('t/copy', $this->strcopy, $this->qbank->copy_question_url($question->id));
+        }
+    }
+}
diff --git a/question/classes/bank/creator_name_column.php b/question/classes/bank/creator_name_column.php
new file mode 100644 (file)
index 0000000..6b13efa
--- /dev/null
@@ -0,0 +1,63 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+
+namespace core_question\bank;
+
+/**
+ * A column type for the name of the question creator.
+ *
+ * @copyright  2009 Tim Hunt
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+class creator_name_column extends column_base {
+    public function get_name() {
+        return 'creatorname';
+    }
+
+    protected function get_title() {
+        return get_string('createdby', 'question');
+    }
+
+    protected function display_content($question, $rowclasses) {
+        if (!empty($question->creatorfirstname) && !empty($question->creatorlastname)) {
+            $u = new \stdClass();
+            $u = username_load_fields_from_object($u, $question, 'creator');
+            echo fullname($u);
+        }
+    }
+
+    public function get_extra_joins() {
+        return array('uc' => 'LEFT JOIN {user} uc ON uc.id = q.createdby');
+    }
+
+    public function get_required_fields() {
+        $allnames = get_all_user_name_fields();
+        $requiredfields = array();
+        foreach ($allnames as $allname) {
+            $requiredfields[] = 'uc.' . $allname . ' AS creator' . $allname;
+        }
+        return $requiredfields;
+    }
+
+    public function is_sortable() {
+        return array(
+            'firstname' => array('field' => 'uc.firstname', 'title' => get_string('firstname')),
+            'lastname' => array('field' => 'uc.lastname', 'title' => get_string('lastname')),
+        );
+    }
+}
diff --git a/question/classes/bank/delete_action_column.php b/question/classes/bank/delete_action_column.php
new file mode 100644 (file)
index 0000000..7ef065e
--- /dev/null
@@ -0,0 +1,56 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+namespace core_question\bank;
+
+/**
+ * action to delete (or hide) a question, or restore a previously hidden question.
+ *
+ * @copyright  2009 Tim Hunt
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+class delete_action_column extends action_column_base {
+    protected $strdelete;
+    protected $strrestore;
+
+    public function init() {
+        parent::init();
+        $this->strdelete = get_string('delete');
+        $this->strrestore = get_string('restore');
+    }
+
+    public function get_name() {
+        return 'deleteaction';
+    }
+
+    protected function display_content($question, $rowclasses) {
+        if (question_has_capability_on($question, 'edit')) {
+            if ($question->hidden) {
+                $url = new \moodle_url($this->qbank->base_url(), array('unhide' => $question->id, 'sesskey' => sesskey()));
+                $this->print_icon('t/restore', $this->strrestore, $url);
+            } else {
+                $url = new \moodle_url($this->qbank->base_url(), array('deleteselected' => $question->id, 'q' . $question->id => 1,
+                                              'sesskey' => sesskey()));
+                $this->print_icon('t/delete', $this->strdelete, $url);
+            }
+        }
+    }
+
+    public function get_required_fields() {
+        return array('q.id', 'q.hidden');
+    }
+}
diff --git a/question/classes/bank/edit_action_column.php b/question/classes/bank/edit_action_column.php
new file mode 100644 (file)
index 0000000..4045969
--- /dev/null
@@ -0,0 +1,46 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+namespace core_question\bank;
+
+/**
+ * Base class for question bank columns that just contain an action icon.
+ *
+ * @copyright  2009 Tim Hunt
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class edit_action_column extends action_column_base {
+    protected $stredit;
+    protected $strview;
+
+    public function init() {
+        parent::init();
+        $this->stredit = get_string('edit');
+        $this->strview = get_string('view');
+    }
+
+    public function get_name() {
+        return 'editaction';
+    }
+
+    protected function display_content($question, $rowclasses) {
+        if (question_has_capability_on($question, 'edit')) {
+            $this->print_icon('t/edit', $this->stredit, $this->qbank->edit_question_url($question->id));
+        } else if (question_has_capability_on($question, 'view')) {
+            $this->print_icon('i/info', $this->strview, $this->qbank->edit_question_url($question->id));
+        }
+    }
+}
diff --git a/question/classes/bank/modifier_name_column.php b/question/classes/bank/modifier_name_column.php
new file mode 100644 (file)
index 0000000..6981f34
--- /dev/null
@@ -0,0 +1,62 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+
+namespace core_question\bank;
+
+/**
+ * A column type for the name of the question last modifier.
+ *
+ * @copyright  2009 Tim Hunt
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class modifier_name_column extends column_base {
+    public function get_name() {
+        return 'modifiername';
+    }
+
+    protected function get_title() {
+        return get_string('lastmodifiedby', 'question');
+    }
+
+    protected function display_content($question, $rowclasses) {
+        if (!empty($question->modifierfirstname) && !empty($question->modifierlastname)) {
+            $u = new \stdClass();
+            $u = username_load_fields_from_object($u, $question, 'modifier');
+            echo fullname($u);
+        }
+    }
+
+    public function get_extra_joins() {
+        return array('um' => 'LEFT JOIN {user} um ON um.id = q.modifiedby');
+    }
+
+    public function get_required_fields() {
+        $allnames = get_all_user_name_fields();
+        $requiredfields = array();
+        foreach ($allnames as $allname) {
+            $requiredfields[] = 'um.' . $allname . ' AS modifier' . $allname;
+        }
+        return $requiredfields;
+    }
+
+    public function is_sortable() {
+        return array(
+            'firstname' => array('field' => 'um.firstname', 'title' => get_string('firstname')),
+            'lastname' => array('field' => 'um.lastname', 'title' => get_string('lastname')),
+        );
+    }
+}
diff --git a/question/classes/bank/preview_action_column.php b/question/classes/bank/preview_action_column.php
new file mode 100644 (file)
index 0000000..f31d5cc
--- /dev/null
@@ -0,0 +1,41 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+namespace core_question\bank;
+
+/**
+ * Question bank columns for the preview action icon.
+ *
+ * @copyright  2009 Tim Hunt
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class preview_action_column extends action_column_base {
+    public function get_name() {
+        return 'previewaction';
+    }
+
+    protected function display_content($question, $rowclasses) {
+        global $PAGE;
+        if (question_has_capability_on($question, 'use')) {
+            echo $PAGE->get_renderer('core_question')->question_preview_link(
+                    $question->id, $this->qbank->get_most_specific_context(), false);
+        }
+    }
+
+    public function get_required_fields() {
+        return array('q.id');
+    }
+}
diff --git a/question/classes/bank/question_name_column.php b/question/classes/bank/question_name_column.php
new file mode 100644 (file)
index 0000000..05d18c6
--- /dev/null
@@ -0,0 +1,65 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+namespace core_question\bank;
+
+/**
+ * A column type for the name of the question name.
+ *
+ * @copyright  2009 Tim Hunt
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class question_name_column extends column_base {
+    protected $checkboxespresent = null;
+
+    public function get_name() {
+        return 'questionname';
+    }
+
+    protected function get_title() {
+        return get_string('question');
+    }
+
+    protected function label_for($question) {
+        if (is_null($this->checkboxespresent)) {
+            $this->checkboxespresent = $this->qbank->has_column('core_question\bank\checkbox_column');
+        }
+        if ($this->checkboxespresent) {
+            return 'checkq' . $question->id;
+        } else {
+            return '';
+        }
+    }
+
+    protected function display_content($question, $rowclasses) {
+        $labelfor = $this->label_for($question);
+        if ($labelfor) {
+            echo '<label for="' . $labelfor . '">';
+        }
+        echo format_string($question->name);
+        if ($labelfor) {
+            echo '</label>';
+        }
+    }
+
+    public function get_required_fields() {
+        return array('q.id', 'q.name');
+    }
+
+    public function is_sortable() {
+        return 'q.name';
+    }
+}
diff --git a/question/classes/bank/question_text_row.php b/question/classes/bank/question_text_row.php
new file mode 100644 (file)
index 0000000..cf9df12
--- /dev/null
@@ -0,0 +1,62 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+
+namespace core_question\bank;
+
+/**
+ * A column type for the name of the question name.
+ *
+ * @copyright  2009 Tim Hunt
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class question_text_row extends row_base {
+    protected $formatoptions;
+
+    protected function init() {
+        $this->formatoptions = new \stdClass();
+        $this->formatoptions->noclean = true;
+        $this->formatoptions->para = false;
+    }
+
+    public function get_name() {
+        return 'questiontext';
+    }
+
+    protected function get_title() {
+        return get_string('questiontext', 'question');
+    }
+
+    protected function display_content($question, $rowclasses) {
+        $text = question_rewrite_question_preview_urls($question->questiontext, $question->id,
+                $question->contextid, 'question', 'questiontext', $question->id,
+                $question->contextid, 'core_question');
+        $text = format_text($text, $question->questiontextformat,
+                $this->formatoptions);
+        if ($text == '') {
+            $text = '&#160;';
+        }
+        echo $text;
+    }
+
+    public function get_extra_joins() {
+        return array('qc' => 'JOIN {question_categories} qc ON qc.id = q.category');
+    }
+
+    public function get_required_fields() {
+        return array('q.id', 'q.questiontext', 'q.questiontextformat', 'qc.contextid');
+    }
+}
diff --git a/question/classes/bank/question_type_column.php b/question/classes/bank/question_type_column.php
new file mode 100644 (file)
index 0000000..da7c839
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+namespace core_question\bank;
+
+/**
+ * A column type for the name of the question type.
+ *
+ * @copyright  2009 Tim Hunt
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class question_type_column extends column_base {
+    public function get_name() {
+        return 'qtype';
+    }
+
+    protected function get_title() {
+        return get_string('qtypeveryshort', 'question');
+    }
+
+    protected function get_title_tip() {
+        return get_string('questiontype', 'question');
+    }
+
+    protected function display_content($question, $rowclasses) {
+        echo print_question_icon($question);
+    }
+
+    public function get_required_fields() {
+        return array('q.qtype');
+    }
+
+    public function is_sortable() {
+        return 'q.qtype';
+    }
+}
diff --git a/question/classes/bank/row_base.php b/question/classes/bank/row_base.php
new file mode 100644 (file)
index 0000000..a091c95
--- /dev/null
@@ -0,0 +1,42 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+namespace core_question\bank;
+
+/**
+ * Base class for 'columns' that are actually displayed as a row following the main question row.
+ *
+ * @copyright  2009 Tim Hunt
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class row_base extends column_base {
+    public function is_extra_row() {
+        return true;
+    }
+
+    protected function display_start($question, $rowclasses) {
+        if ($rowclasses) {
+            echo '<tr class="' . $rowclasses . '">' . "\n";
+        } else {
+            echo "<tr>\n";
+        }
+        echo '<td colspan="' . $this->qbank->get_column_count() . '" class="' . $this->get_name() . '">';
+    }
+
+    protected function display_end($question, $rowclasses) {
+        echo "</td></tr>\n";
+    }
+}
diff --git a/question/classes/bank/view.php b/question/classes/bank/view.php
new file mode 100644 (file)
index 0000000..022be82
--- /dev/null
@@ -0,0 +1,923 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+
+namespace core_question\bank;
+
+/**
+ * Functions used to show question editing interface
+ *
+ * @package    moodlecore
+ * @subpackage questionbank
+ * @copyright  1999 onwards Martin Dougiamas and others {@link http://moodle.com}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+/**
+ * This class prints a view of the question bank, including
+ *  + Some controls to allow users to to select what is displayed.
+ *  + A list of questions as a table.
+ *  + Further controls to do things with the questions.
+ *
+ * This class gives a basic view, and provides plenty of hooks where subclasses
+ * can override parts of the display.
+ *
+ * The list of questions presented as a table is generated by creating a list of
+ * core_question\bank\column objects, one for each 'column' to be displayed. These
+ * manage
+ *  + outputting the contents of that column, given a $question object, but also
+ *  + generating the right fragments of SQL to ensure the necessary data is present,
+ *    and sorted in the right order.
+ *  + outputting table headers.
+ *
+ * @copyright  2009 Tim Hunt
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class view {
+    const MAX_SORTS = 3;
+
+    protected $baseurl;
+    protected $editquestionurl;
+    protected $quizorcourseid;
+    protected $contexts;
+    protected $cm;
+    protected $course;
+    protected $visiblecolumns;
+    protected $extrarows;
+    protected $requiredcolumns;
+    protected $sort;
+    protected $lastchangedid;
+    protected $countsql;
+    protected $loadsql;
+    protected $sqlparams;
+    /** @var array of \core_question\bank\search\condition objects. */
+    protected $searchconditions = array();
+
+    /**
+     * Constructor
+     * @param question_edit_contexts $contexts
+     * @param moodle_url $pageurl
+     * @param object $course course settings
+     * @param object $cm (optional) activity settings.
+     */
+    public function __construct($contexts, $pageurl, $course, $cm = null) {
+        global $CFG, $PAGE;
+
+        $this->contexts = $contexts;
+        $this->baseurl = $pageurl;
+        $this->course = $course;
+        $this->cm = $cm;
+
+        if (!empty($cm) && $cm->modname == 'quiz') {
+            $this->quizorcourseid = '&amp;quizid=' . $cm->instance;
+        } else {
+            $this->quizorcourseid = '&amp;courseid=' .$this->course->id;
+        }
+
+        // Create the url of the new question page to forward to.
+        $returnurl = $pageurl->out_as_local_url(false);
+        $this->editquestionurl = new \moodle_url('/question/question.php',
+                array('returnurl' => $returnurl));
+        if ($cm !== null) {
+            $this->editquestionurl->param('cmid', $cm->id);
+        } else {
+            $this->editquestionurl->param('courseid', $this->course->id);
+        }
+
+        $this->lastchangedid = optional_param('lastchanged', 0, PARAM_INT);
+
+        $this->init_columns($this->wanted_columns(), $this->heading_column());
+        $this->init_sort();
+        $this->init_search_conditions($this->contexts, $this->course, $this->cm);
+    }
+
+    /**
+     * Initialize search conditions from plugins
+     * local_*_get_question_bank_search_conditions() must return an array of
+     * \core_question\bank\search\condition objects.
+     */
+    protected function init_search_conditions() {
+        $searchplugins = get_plugin_list_with_function('local', 'get_question_bank_search_conditions');
+        foreach ($searchplugins as $component => $function) {
+            foreach ($function($this) as $searchobject) {
+                $this->add_searchcondition($searchobject);
+            }
+        }
+    }
+
+    protected function wanted_columns() {
+        global $CFG;
+
+        if (empty($CFG->questionbankcolumns)) {
+            $questionbankcolumns = array('checkbox_column', 'question_type_column',
+                                     'question_name_column', 'edit_action_column', 'copy_action_column',
+                                     'preview_action_column', 'delete_action_column',
+                                     'creator_name_column',
+                                     'modifier_name_column');
+        } else {
+             $questionbankcolumns = explode(',', $CFG->questionbankcolumns);
+        }
+        if (question_get_display_preference('qbshowtext', 0, PARAM_BOOL, new \moodle_url(''))) {
+            $questionbankcolumns[] = 'question_text_row';
+        }
+
+        foreach ($questionbankcolumns as $fullname) {
+            if (! class_exists($fullname)) {
+                if (class_exists('core_question\\bank\\' . $fullname)) {
+                    $fullname = 'core_question\\bank\\' . $fullname;
+                } else {
+                    throw new \coding_exception("No such class exists: $fullname");
+                }
+            }
+            $this->requiredcolumns[$fullname] = new $fullname($this);
+        }
+        return $this->requiredcolumns;
+    }
+
+
+    /**
+     * Get a column object from its name.
+     *
+     * @param string $columnname.
+     * @return \core_question\bank\column_base.
+     */
+    protected function get_column_type($columnname) {
+        if (! class_exists($columnname)) {
+            if (class_exists('core_question\\bank\\' . $columnname)) {
+                $columnname = 'core_question\\bank\\' . $columnname;
+            } else {
+                throw new \coding_exception("No such class exists: $columnname");
+            }
+        }
+        if (empty($this->requiredcolumns[$columnname])) {
+            $this->requiredcolumns[$columnname] = new $columnname($this);
+        }
+        return $this->requiredcolumns[$columnname];
+    }
+
+    /**
+     * Specify the column heading
+     *
+     * @return string Column name for the heading
+     */
+    protected function heading_column() {
+        return 'question_bank_question_name_column';
+    }
+
+    /**
+     * Initializing table columns
+     *
+     * @param array $wanted Collection of column names
+     * @param string $heading The name of column that is set as heading
+     */
+    protected function init_columns($wanted, $heading = '') {
+        $this->visiblecolumns = array();
+        $this->extrarows = array();
+        foreach ($wanted as $column) {
+            if ($column->is_extra_row()) {
+                $this->extrarows[get_class($column)] = $column;
+            } else {
+                $this->visiblecolumns[get_class($column)] = $column;
+            }
+        }
+        if (array_key_exists($heading, $this->requiredcolumns)) {
+            $this->requiredcolumns[$heading]->set_as_heading();
+        }
+    }
+
+    /**
+     * @param string $colname a column internal name.
+     * @return bool is this column included in the output?
+     */
+    public function has_column($colname) {
+        return isset($this->visiblecolumns[$colname]);
+    }
+
+    /**
+     * @return int The number of columns in the table.
+     */
+    public function get_column_count() {
+        return count($this->visiblecolumns);
+    }
+
+    public function get_courseid() {
+        return $this->course->id;
+    }
+
+    protected function init_sort() {
+        $this->init_sort_from_params();
+        if (empty($this->sort)) {
+            $this->sort = $this->default_sort();
+        }
+    }
+
+    /**
+     * Deal with a sort name of the form columnname, or colname_subsort by
+     * breaking it up, validating the bits that are presend, and returning them.
+     * If there is no subsort, then $subsort is returned as ''.
+     * @return array array($colname, $subsort).
+     */
+    protected function parse_subsort($sort) {
+        // Do the parsing.
+        if (strpos($sort, '-') !== false) {
+            list($colname, $subsort) = explode('-', $sort, 2);
+        } else {
+            $colname = $sort;
+            $subsort = '';
+        }
+        // Validate the column name.
+        $column = $this->get_column_type($colname);
+        if (!isset($column) || !$column->is_sortable()) {
+            for ($i = 1; $i <= self::MAX_SORTS; $i++) {
+                $this->baseurl->remove_params('qbs' . $i);
+            }
+            throw new \moodle_exception('unknownsortcolumn', '', $link = $this->baseurl->out(), $colname);
+        }
+        // Validate the subsort, if present.
+        if ($subsort) {
+            $subsorts = $column->is_sortable();
+            if (!is_array($subsorts) || !isset($subsorts[$subsort])) {
+                throw new \moodle_exception('unknownsortcolumn', '', $link = $this->baseurl->out(), $sort);
+            }
+        }
+        return array($colname, $subsort);
+    }
+
+    protected function init_sort_from_params() {
+        $this->sort = array();
+        for ($i = 1; $i <= self::MAX_SORTS; $i++) {
+            if (!$sort = optional_param('qbs' . $i, '', PARAM_TEXT)) {
+                break;
+            }
+            // Work out the appropriate order.
+            $order = 1;
+            if ($sort[0] == '-') {
+                $order = -1;
+                $sort = substr($sort, 1);
+                if (!$sort) {
+                    break;
+                }
+            }
+            // Deal with subsorts.
+            list($colname, $subsort) = $this->parse_subsort($sort);
+            $this->requiredcolumns[$colname] = $this->get_column_type($colname);
+            $this->sort[$sort] = $order;
+        }
+    }
+
+    protected function sort_to_params($sorts) {
+        $params = array();
+        $i = 0;
+        foreach ($sorts as $sort => $order) {
+            $i += 1;
+            if ($order < 0) {
+                $sort = '-' . $sort;
+            }
+            $params['qbs' . $i] = $sort;
+        }
+        return $params;
+    }
+
+    protected function default_sort() {
+        return array('core_question\bank\question_type_column' => 1, 'core_question\bank\question_name_column' => 1);
+    }
+
+    /**
+     * @param $sort a column or column_subsort name.
+     * @return int the current sort order for this column -1, 0, 1
+     */
+    public function get_primary_sort_order($sort) {
+        $order = reset($this->sort);
+        $primarysort = key($this->sort);
+        if ($sort == $primarysort) {
+            return $order;
+        } else {
+            return 0;
+        }
+    }
+
+    /**
+     * Get a URL to redisplay the page with a new sort for the question bank.
+     * @param string $sort the column, or column_subsort to sort on.
+     * @param bool $newsortreverse whether to sort in reverse order.
+     * @return string The new URL.
+     */
+    public function new_sort_url($sort, $newsortreverse) {
+        if ($newsortreverse) {
+            $order = -1;
+        } else {
+            $order = 1;
+        }
+        // Tricky code to add the new sort at the start, removing it from where it was before, if it was present.
+        $newsort = array_reverse($this->sort);
+        if (isset($newsort[$sort])) {
+            unset($newsort[$sort]);
+        }
+        $newsort[$sort] = $order;
+        $newsort = array_reverse($newsort);
+        if (count($newsort) > self::MAX_SORTS) {
+            $newsort = array_slice($newsort, 0, self::MAX_SORTS, true);
+        }
+        return $this->baseurl->out(true, $this->sort_to_params($newsort));
+    }
+
+    /**
+     * Create the SQL query to retrieve the indicated questions
+     * @param stdClass $category no longer used.
+     * @param bool $recurse no longer used.
+     * @param bool $showhidden no longer used.
+     * @deprecated since Moodle 2.7 MDL-40313.
+     * @see build_query()
+     * @see \core_question\bank\search\condition
+     * @todo MDL-41978 This will be deleted in Moodle 2.8
+     */
+    protected function build_query_sql($category, $recurse, $showhidden) {
+        debugging('build_query_sql() is deprecated, please use \core_question\bank\view::build_query() and ' .
+                '\core_question\bank\search\condition classes instead.', DEBUG_DEVELOPER);
+        self::build_query();
+    }
+
+    /**
+     * Create the SQL query to retrieve the indicated questions, based on
+     * \core_question\bank\search\condition filters.
+     */
+    protected function build_query() {
+        global $DB;
+
+        // Get the required tables and fields.
+        $joins = array();
+        $fields = array('q.hidden', 'q.category');
+        foreach ($this->requiredcolumns as $column) {
+            $extrajoins = $column->get_extra_joins();
+            foreach ($extrajoins as $prefix => $join) {
+                if (isset($joins[$prefix]) && $joins[$prefix] != $join) {
+                    throw new \coding_exception('Join ' . $join . ' conflicts with previous join ' . $joins[$prefix]);
+                }
+                $joins[$prefix] = $join;
+            }
+            $fields = array_merge($fields, $column->get_required_fields());
+        }
+        $fields = array_unique($fields);
+
+        // Build the order by clause.
+        $sorts = array();
+        foreach ($this->sort as $sort => $order) {
+            list($colname, $subsort) = $this->parse_subsort($sort);
+            $sorts[] = $this->requiredcolumns[$colname]->sort_expression($order < 0, $subsort);
+        }
+
+        // Build the where clause.
+        $tests = array('q.parent = 0');
+        $this->sqlparams = array();
+        foreach ($this->searchconditions as $searchcondition) {
+            if ($searchcondition->where()) {
+                $tests[] = '((' . $searchcondition->where() .'))';
+            }
+            if ($searchcondition->params()) {
+                $this->sqlparams = array_merge($this->sqlparams, $searchcondition->params());
+            }
+        }
+        // Build the SQL.
+        $sql = ' FROM {question} q ' . implode(' ', $joins);
+        $sql .= ' WHERE ' . implode(' AND ', $tests);
+        $this->countsql = 'SELECT count(1)' . $sql;
+        $this->loadsql = 'SELECT ' . implode(', ', $fields) . $sql . ' ORDER BY ' . implode(', ', $sorts);
+    }
+
+    protected function get_question_count() {
+        global $DB;
+        return $DB->count_records_sql($this->countsql, $this->sqlparams);
+    }
+
+    protected function load_page_questions($page, $perpage) {
+        global $DB;
+        $questions = $DB->get_recordset_sql($this->loadsql, $this->sqlparams, $page * $perpage, $perpage);
+        if (!$questions->valid()) {
+            // No questions on this page. Reset to page 0.
+            $questions = $DB->get_recordset_sql($this->loadsql, $this->sqlparams, 0, $perpage);
+        }
+        return $questions;
+    }
+
+    public function base_url() {
+        return $this->baseurl;
+    }
+
+    public function edit_question_url($questionid) {
+        return $this->editquestionurl->out(true, array('id' => $questionid));
+    }
+
+    /**
+     * Get the URL for duplicating a given question.
+     * @param int $questionid the question id.
+     * @return moodle_url the URL.
+     */
+    public function copy_question_url($questionid) {
+        return $this->editquestionurl->out(true, array('id' => $questionid, 'makecopy' => 1));
+    }
+
+    /**
+     * Get the context we are displaying the question bank for.
+     * @return context context object.
+     */
+    public function get_most_specific_context() {
+        return $this->contexts->lowest();
+    }
+
+    /**
+     * Get the URL to preview a question.
+     * @param stdClass $questiondata the data defining the question.
+     * @return moodle_url the URL.
+     */
+    public function preview_question_url($questiondata) {
+        return question_preview_url($questiondata->id, null, null, null, null,
+                $this->get_most_specific_context());
+    }
+
+    /**
+     * Shows the question bank editing interface.
+     *
+     * The function also processes a number of actions:
+     *
+     * Actions affecting the question pool:
+     * move           Moves a question to a different category
+     * deleteselected Deletes the selected questions from the category
+     * Other actions:
+     * category      Chooses the category
+     * displayoptions Sets display options
+     */
+    public function display($tabname, $page, $perpage, $cat,
+            $recurse, $showhidden, $showquestiontext) {
+        global $PAGE, $OUTPUT;
+
+        if ($this->process_actions_needing_ui()) {
+            return;
+        }
+        $editcontexts = $this->contexts->having_one_edit_tab_cap($tabname);
+        // Category selection form.
+        echo $OUTPUT->heading(get_string('questionbank', 'question'), 2);
+        array_unshift($this->searchconditions, new \core_question\bank\search\hidden_condition(!$showhidden));
+        array_unshift($this->searchconditions, new \core_question\bank\search\category_condition(
+                $cat, $recurse, $editcontexts, $this->baseurl, $this->course));
+        $this->display_options_form($showquestiontext);
+
+        // Continues with list of questions.
+        $this->display_question_list($this->contexts->having_one_edit_tab_cap($tabname),
+                $this->baseurl, $cat, $this->cm,
+                null, $page, $perpage, $showhidden, $showquestiontext,
+                $this->contexts->having_cap('moodle/question:add'));
+    }
+
+    protected function print_choose_category_message($categoryandcontext) {
+        echo "<p style=\"text-align:center;\"><b>";
+        print_string('selectcategoryabove', 'question');
+        echo "</b></p>";
+    }
+
+    protected function get_current_category($categoryandcontext) {
+        global $DB, $OUTPUT;
+        list($categoryid, $contextid) = explode(',', $categoryandcontext);
+        if (!$categoryid) {
+            $this->print_choose_category_message($categoryandcontext);
+            return false;
+        }
+
+        if (!$category = $DB->get_record('question_categories',
+                array('id' => $categoryid, 'contextid' => $contextid))) {
+            echo $OUTPUT->box_start('generalbox questionbank');
+            echo $OUTPUT->notification('Category not found!');
+            echo $OUTPUT->box_end();
+            return false;
+        }
+
+        return $category;
+    }
+
+    /**
+     * prints category information
+     * @param stdClass $category the category row from the database.
+     * @deprecated since Moodle 2.7 MDL-40313.
+     * @see \core_question\bank\search\condition
+     * @todo MDL-41978 This will be deleted in Moodle 2.8
+     */
+    protected function print_category_info($category) {
+        $formatoptions = new \stdClass();
+        $formatoptions->noclean = true;
+        $formatoptions->overflowdiv = true;
+        echo '<div class="boxaligncenter">';
+        echo format_text($category->info, $category->infoformat, $formatoptions, $this->course->id);
+        echo "</div>\n";
+    }
+
+    /**
+     * Prints a form to choose categories
+     * @deprecated since Moodle 2.7 MDL-40313.
+     * @see \core_question\bank\search\condition
+     * @todo MDL-41978 This will be deleted in Moodle 2.8
+     */
+    protected function display_category_form($contexts, $pageurl, $current) {
+        global $OUTPUT;
+
+        debugging('display_category_form() is deprecated, please use ' .
+                '\core_question\bank\search\condition instead.', DEBUG_DEVELOPER);
+        // Get all the existing categories now.
+        echo '<div class="choosecategory">';
+        $catmenu = question_category_options($contexts, false, 0, true);
+
+        $select = new \single_select($this->baseurl, 'category', $catmenu, $current, null, 'catmenu');
+        $select->set_label(get_string('selectacategory', 'question'));
+        echo $OUTPUT->render($select);
+        echo "</div>\n";
+    }
+
+    /**
+     * Display the options form.
+     * @param bool $recurse no longer used.
+     * @param bool $showhidden no longer used.
+     * @param bool $showquestiontext whether to show the question text.
+     * @deprecated since Moodle 2.7 MDL-40313.
+     * @see display_options_form
+     * @todo MDL-41978 This will be deleted in Moodle 2.8
+     * @see \core_question\bank\search\condition
+     */
+    protected function display_options($recurse, $showhidden, $showquestiontext) {
+        debugging('display_options() is deprecated, please use display_options_form instead.', DEBUG_DEVELOPER);
+        return $this->display_options_form($showquestiontext);
+    }
+
+    /**
+     * Print a single option checkbox.
+     * @deprecated since Moodle 2.7 MDL-40313.
+     * @see \core_question\bank\search\condition
+     * @see html_writer::checkbox
+     * @todo MDL-41978 This will be deleted in Moodle 2.8
+     */
+    protected function display_category_form_checkbox($name, $value, $label) {
+        debugging('display_category_form_checkbox() is deprecated, ' .
+                'please use \core_question\bank\search\condition instead.', DEBUG_DEVELOPER);
+        echo '<div><input type="hidden" id="' . $name . '_off" name="' . $name . '" value="0" />';
+        echo '<input type="checkbox" id="' . $name . '_on" name="' . $name . '" value="1"';
+        if ($value) {
+            echo ' checked="checked"';
+        }
+        echo ' onchange="getElementById(\'displayoptions\').submit(); return true;" />';
+        echo '<label for="' . $name . '_on">' . $label . '</label>';
+        echo "</div>\n";
+    }
+
+    /**
+     * Display the form with options for which questions are displayed and how they are displayed.
+     * @param bool $showquestiontext Display the text of the question within the list.
+     */
+    protected function display_options_form($showquestiontext) {
+        global $PAGE;
+
+        echo '<form method="get" action="edit.php" id="displayoptions">';
+        echo "<fieldset class='invisiblefieldset'>";
+        echo \html_writer::input_hidden_params($this->baseurl, array('recurse', 'showhidden', 'qbshowtext'));
+
+        foreach ($this->searchconditions as $searchcondition) {
+            echo $searchcondition->display_options($this);
+        }
+        $this->display_showtext_checkbox($showquestiontext);
+        $this->display_advanced_search_form();
+        $PAGE->requires->yui_module('moodle-question-searchform', 'M.question.searchform.init');
+        echo '<noscript><div class="centerpara"><input type="submit" value="'. get_string('go') .'" />';
+        echo '</div></noscript></fieldset></form>';
+    }
+
+    /**
+     * Print the "advanced" UI elements for the form to select which questions. Hidden by default.
+     */
+    protected function display_advanced_search_form() {
+        print_collapsible_region_start('', 'advancedsearch', get_string('advancedsearchoptions', 'question'),
+                                               'question_bank_advanced_search');
+        foreach ($this->searchconditions as $searchcondition) {
+            echo $searchcondition->display_options_adv($this);
+        }
+        print_collapsible_region_end();
+    }
+
+    /**
+     * Display the checkbox UI for toggling the display of the question text in the list.
+     * @param bool $showquestiontext the current or default value for whether to display the text.
+     */
+    protected function display_showtext_checkbox($showquestiontext) {
+        echo '<div>';
+        echo \html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'qbshowtext',
+                                               'value' => 0, 'id' => 'qbshowtext_off'));
+        echo \html_writer::checkbox('qbshowtext', '1', $showquestiontext, get_string('showquestiontext', 'question'),
+                                       array('id' => 'qbshowtext_on', 'class' => 'searchoptions'));
+        echo "</div>\n";
+    }
+
+    protected function create_new_question_form($category, $canadd) {
+        global $CFG;
+        echo '<div class="createnewquestion">';
+        if ($canadd) {
+            create_new_question_button($category->id, $this->editquestionurl->params(),
+                    get_string('createnewquestion', 'question'));
+        } else {
+            print_string('nopermissionadd', 'question');
+        }
+        echo '</div>';
+    }
+
+    /**
+     * Prints the table of questions in a category with interactions
+     *
+     * @param array      $contexts    Not used!
+     * @param moodle_url $pageurl     The URL to reload this page.
+     * @param string     $categoryandcontext 'categoryID,contextID'.
+     * @param stdClass   $cm          Not used!
+     * @param bool       $recurse     Whether to include subcategories.
+     * @param int        $page        The number of the page to be displayed
+     * @param int        $perpage     Number of questions to show per page
+     * @param bool       $showhidden  whether deleted questions should be displayed.
+     * @param bool       $showquestiontext whether the text of each question should be shown in the list. Deprecated.
+     * @param array      $addcontexts contexts where the user is allowed to add new questions.
+     */
+    protected function display_question_list($contexts, $pageurl, $categoryandcontext,
+            $cm = null, $recurse=1, $page=0, $perpage=100, $showhidden=false,
+            $showquestiontext = false, $addcontexts = array()) {
+        global $CFG, $DB, $OUTPUT;
+
+        $category = $this->get_current_category($categoryandcontext);
+
+        $cmoptions = new \stdClass();
+        $cmoptions->hasattempts = !empty($this->quizhasattempts);
+
+        $strselectall = get_string('selectall');
+        $strselectnone = get_string('deselectall');
+        $strdelete = get_string('delete');
+
+        list($categoryid, $contextid) = explode(',', $categoryandcontext);
+        $catcontext = \context::instance_by_id($contextid);
+
+        $canadd = has_capability('moodle/question:add', $catcontext);
+        $caneditall = has_capability('moodle/question:editall', $catcontext);
+        $canuseall = has_capability('moodle/question:useall', $catcontext);
+        $canmoveall = has_capability('moodle/question:moveall', $catcontext);
+
+        $this->create_new_question_form($category, $canadd);
+
+        $this->build_query();
+        $totalnumber = $this->get_question_count();
+        if ($totalnumber == 0) {
+            return;
+        }
+        $questions = $this->load_page_questions($page, $perpage);
+
+        echo '<div class="categorypagingbarcontainer">';
+        $pageingurl = new \moodle_url('edit.php');
+        $r = $pageingurl->params($pageurl->params());
+        $pagingbar = new \paging_bar($totalnumber, $page, $perpage, $pageingurl);
+        $pagingbar->pagevar = 'qpage';
+        echo $OUTPUT->render($pagingbar);
+        echo '</div>';
+
+        echo '<form method="post" action="edit.php">';
+        echo '<fieldset class="invisiblefieldset" style="display: block;">';
+        echo '<input type="hidden" name="sesskey" value="'.sesskey().'" />';
+        echo \html_writer::input_hidden_params($this->baseurl);
+
+        echo '<div class="categoryquestionscontainer">';
+        $this->start_table();
+        $rowcount = 0;
+        foreach ($questions as $question) {
+            $this->print_table_row($question, $rowcount);
+            $rowcount += 1;
+        }
+        $this->end_table();
+        echo "</div>\n";
+
+        echo '<div class="categorypagingbarcontainer pagingbottom">';
+        echo $OUTPUT->render($pagingbar);
+        if ($totalnumber > DEFAULT_QUESTIONS_PER_PAGE) {
+            if ($perpage == DEFAULT_QUESTIONS_PER_PAGE) {
+                $url = new \moodle_url('edit.php', array_merge($pageurl->params(), array('qperpage' => 1000)));
+                $showall = '<a href="'.$url.'">'.get_string('showall', 'moodle', $totalnumber).'</a>';
+            } else {
+                $url = new \moodle_url('edit.php', array_merge($pageurl->params(),
+                                              array('qperpage' => DEFAULT_QUESTIONS_PER_PAGE)));
+                $showall = '<a href="'.$url.'">'.get_string('showperpage', 'moodle', DEFAULT_QUESTIONS_PER_PAGE).'</a>';
+            }
+            echo "<div class='paging'>$showall</div>";
+        }
+        echo '</div>';
+
+        echo '<div class="modulespecificbuttonscontainer">';
+        if ($caneditall || $canmoveall || $canuseall) {
+            echo '<strong>&nbsp;'.get_string('withselected', 'question').':</strong><br />';
+
+            if (function_exists('module_specific_buttons')) {
+                echo module_specific_buttons($this->cm->id, $cmoptions);
+            }
+
+            // Print delete and move selected question.
+            if ($caneditall) {
+                echo '<input type="submit" name="deleteselected" value="' . $strdelete . "\" />\n";
+            }
+
+            if ($canmoveall && count($addcontexts)) {
+                echo '<input type="submit" name="move" value="'.get_string('moveto', 'question')."\" />\n";
+                question_category_select_menu($addcontexts, false, 0, "$category->id,$category->contextid");
+            }
+
+            if (function_exists('module_specific_controls') && $canuseall) {
+                $modulespecific = module_specific_controls($totalnumber, $recurse, $category, $this->cm->id, $cmoptions);
+                if (!empty($modulespecific)) {
+                    echo "<hr />$modulespecific";
+                }
+            }
+        }
+        echo "</div>\n";
+
+        echo '</fieldset>';
+        echo "</form>\n";
+    }
+
+    protected function start_table() {
+        echo '<table id="categoryquestions">' . "\n";
+        echo "<thead>\n";
+        $this->print_table_headers();
+        echo "</thead>\n";
+        echo "<tbody>\n";
+    }
+
+    protected function end_table() {
+        echo "</tbody>\n";
+        echo "</table>\n";
+    }
+
+    protected function print_table_headers() {
+        echo "<tr>\n";
+        foreach ($this->visiblecolumns as $column) {
+            $column->display_header();
+        }
+        echo "</tr>\n";
+    }
+
+    protected function get_row_classes($question, $rowcount) {
+        $classes = array();
+        if ($question->hidden) {
+            $classes[] = 'dimmed_text';
+        }
+        if ($question->id == $this->lastchangedid) {
+            $classes[] = 'highlight';
+        }
+        $classes[] = 'r' . ($rowcount % 2);
+        return $classes;
+    }
+
+    protected function print_table_row($question, $rowcount) {
+        $rowclasses = implode(' ', $this->get_row_classes($question, $rowcount));
+        if ($rowclasses) {
+            echo '<tr class="' . $rowclasses . '">' . "\n";
+        } else {
+            echo "<tr>\n";
+        }
+        foreach ($this->visiblecolumns as $column) {
+            $column->display($question, $rowclasses);
+        }
+        echo "</tr>\n";
+        foreach ($this->extrarows as $row) {
+            $row->display($question, $rowclasses);
+        }
+    }
+
+    public function process_actions() {
+        global $CFG, $DB;
+        // Now, check for commands on this page and modify variables as necessary.
+        if (optional_param('move', false, PARAM_BOOL) and confirm_sesskey()) {
+            // Move selected questions to new category.
+            $category = required_param('category', PARAM_SEQUENCE);
+            list($tocategoryid, $contextid) = explode(',', $category);
+            if (! $tocategory = $DB->get_record('question_categories', array('id' => $tocategoryid, 'contextid' => $contextid))) {
+                print_error('cannotfindcate', 'question');
+            }
+            $tocontext = \context::instance_by_id($contextid);
+            require_capability('moodle/question:add', $tocontext);
+            $rawdata = (array) data_submitted();
+            $questionids = array();
+            foreach ($rawdata as $key => $value) {  // Parse input for question ids.
+                if (preg_match('!^q([0-9]+)$!', $key, $matches)) {
+                    $key = $matches[1];
+                    $questionids[] = $key;
+                }
+            }
+            if ($questionids) {
+                list($usql, $params) = $DB->get_in_or_equal($questionids);
+                $sql = "";
+                $questions = $DB->get_records_sql("
+                        SELECT q.*, c.contextid
+                        FROM {question} q
+                        JOIN {question_categories} c ON c.id = q.category
+                        WHERE q.id $usql", $params);
+                foreach ($questions as $question) {
+                    question_require_capability_on($question, 'move');
+                }
+                question_move_questions_to_category($questionids, $tocategory->id);
+                redirect($this->baseurl->out(false,
+                        array('category' => "$tocategoryid,$contextid")));
+            }
+        }
+
+        if (optional_param('deleteselected', false, PARAM_BOOL)) { // Delete selected questions from the category.
+            // If teacher has already confirmed the action.
+            if (($confirm = optional_param('confirm', '', PARAM_ALPHANUM)) and confirm_sesskey()) {
+                $deleteselected = required_param('deleteselected', PARAM_RAW);
+                if ($confirm == md5($deleteselected)) {
+                    if ($questionlist = explode(',', $deleteselected)) {
+                        // For each question either hide it if it is in use or delete it.
+                        foreach ($questionlist as $questionid) {
+                            $questionid = (int)$questionid;
+                            question_require_capability_on($questionid, 'edit');
+                            if (questions_in_use(array($questionid))) {
+                                $DB->set_field('question', 'hidden', 1, array('id' => $questionid));
+                            } else {
+                                question_delete_question($questionid);
+                            }
+                        }
+                    }
+                    redirect($this->baseurl);
+                } else {
+                    print_error('invalidconfirm', 'question');
+                }
+            }
+        }
+
+        // Unhide a question.
+        if (($unhide = optional_param('unhide', '', PARAM_INT)) and confirm_sesskey()) {
+            question_require_capability_on($unhide, 'edit');
+            $DB->set_field('question', 'hidden', 0, array('id' => $unhide));
+
+            // Purge these questions from the cache.
+            \core_question_bank::notify_question_edited($unhide);
+
+            redirect($this->baseurl);
+        }
+    }
+
+    public function process_actions_needing_ui() {
+        global $DB, $OUTPUT;
+        if (optional_param('deleteselected', false, PARAM_BOOL)) {
+            // Make a list of all the questions that are selected.
+            $rawquestions = $_REQUEST; // This code is called by both POST forms and GET links, so cannot use data_submitted.
+            $questionlist = '';  // comma separated list of ids of questions to be deleted
+            $questionnames = ''; // string with names of questions separated by <br /> with
+                                 // an asterix in front of those that are in use
+            $inuse = false;      // set to true if at least one of the questions is in use
+            foreach ($rawquestions as $key => $value) {    // Parse input for question ids.
+                if (preg_match('!^q([0-9]+)$!', $key, $matches)) {
+                    $key = $matches[1];
+                    $questionlist .= $key.',';
+                    question_require_capability_on($key, 'edit');
+                    if (questions_in_use(array($key))) {
+                        $questionnames .= '* ';
+                        $inuse = true;
+                    }
+                    $questionnames .= $DB->get_field('question', 'name', array('id' => $key)) . '<br />';
+                }
+            }
+            if (!$questionlist) { // No questions were selected.
+                redirect($this->baseurl);
+            }
+            $questionlist = rtrim($questionlist, ',');
+
+            // Add an explanation about questions in use.
+            if ($inuse) {
+                $questionnames .= '<br />'.get_string('questionsinuse', 'question');
+            }
+            $baseurl = new \moodle_url('edit.php', $this->baseurl->params());
+            $deleteurl = new \moodle_url($baseurl, array('deleteselected' => $questionlist, 'confirm' => md5($questionlist),
+                                                 'sesskey' => sesskey()));
+
+            echo $OUTPUT->confirm(get_string('deletequestionscheck', 'question', $questionnames), $deleteurl, $baseurl);
+
+            return true;
+        }
+    }
+
+    /**
+     * Add another search control to this view.
+     * @param \core_question\bank\search\condition $searchcondition the condition to add.
+     */
+    public function add_searchcondition($searchcondition) {
+        $this->searchconditions[] = $searchcondition;
+    }
+}
index 58fe57c..b8cdb7f 100644 (file)
@@ -36,7 +36,7 @@ if (($lastchanged = optional_param('lastchanged', 0, PARAM_INT)) !== 0) {
 }
 $PAGE->set_url($url);
 
-$questionbank = new question_bank_view($contexts, $thispageurl, $COURSE, $cm);
+$questionbank = new core_question\bank\view($contexts, $thispageurl, $COURSE, $cm);
 $questionbank->process_actions();
 
 // TODO log this page view.
index 4048c1e..2739700 100644 (file)
@@ -129,362 +129,27 @@ function question_can_delete_cat($todelete) {
  *
  * @copyright  2009 Tim Hunt
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @deprecated since Moodle 2.7 MDL-40457
  */
-abstract class question_bank_column_base {
-    /**
-     * @var question_bank_view
-     */
-    protected $qbank;
-
-    /** @var bool determine whether the column is td or th. */
-    protected $isheading = false;
-
-    /**
-     * Constructor.
-     * @param $qbank the question_bank_view we are helping to render.
-     */
-    public function __construct(question_bank_view $qbank) {
-        $this->qbank = $qbank;
-        $this->init();
-    }
-
-    /**
-     * A chance for subclasses to initialise themselves, for example to load lang strings,
-     * without having to override the constructor.
-     */
-    protected function init() {
-    }
-
-    /**
-     * Set the column as heading
-     */
-    public function set_as_heading() {
-        $this->isheading = true;
-    }
-
-    public function is_extra_row() {
-        return false;
-    }
-
-    /**
-     * Output the column header cell.
-     */
-    public function display_header() {
-        echo '<th class="header ' . $this->get_classes() . '" scope="col">';
-        $sortable = $this->is_sortable();
-        $name = $this->get_name();
-        $title = $this->get_title();
-        $tip = $this->get_title_tip();
-        if (is_array($sortable)) {
-            if ($title) {
-                echo '<div class="title">' . $title . '</div>';
-            }
-            $links = array();
-            foreach ($sortable as $subsort => $details) {
-                $links[] = $this->make_sort_link($name . '_' . $subsort,
-                        $details['title'], '', !empty($details['reverse']));
-            }
-            echo '<div class="sorters">' . implode(' / ', $links) . '</div>';
-        } else if ($sortable) {
-            echo $this->make_sort_link($name, $title, $tip);
-        } else {
-            if ($tip) {
-                echo '<span title="' . $tip . '">';
-            }
-            echo $title;
-            if ($tip) {
-                echo '</span>';
-            }
-        }
-        echo "</th>\n";
-    }
-
-    /**
-     * Title for this column. Not used if is_sortable returns an array.
-     * @param object $question the row from the $question table, augmented with extra information.
-     * @param string $rowclasses CSS class names that should be applied to this row of output.
-     */
-    protected abstract function get_title();
-
-    /**
-     * @return string a fuller version of the name. Use this when get_title() returns
-     * something very short, and you want a longer version as a tool tip.
-     */
-    protected function get_title_tip() {
-        return '';
-    }
-
-    /**
-     * Get a link that changes the sort order, and indicates the current sort state.
-     * @param $name internal name used for this type of sorting.
-     * @param $currentsort the current sort order -1, 0, 1 for descending, none, ascending.
-     * @param $title the link text.
-     * @param $defaultreverse whether the default sort order for this column is descending, rather than ascending.
-     * @return string HTML fragment.
-     */
-    protected function make_sort_link($sort, $title, $tip, $defaultreverse = false) {
-        $currentsort = $this->qbank->get_primary_sort_order($sort);
-        $newsortreverse = $defaultreverse;
-        if ($currentsort) {
-            $newsortreverse = $currentsort > 0;
-        }
-        if (!$tip) {
-            $tip = $title;
-        }
-        if ($newsortreverse) {
-            $tip = get_string('sortbyxreverse', '', $tip);
-        } else {
-            $tip = get_string('sortbyx', '', $tip);
-        }
-        $link = '<a href="' . $this->qbank->new_sort_url($sort, $newsortreverse) . '" title="' . $tip . '">';
-        $link .= $title;
-        if ($currentsort) {
-            $link .= $this->get_sort_icon($currentsort < 0);
-        }
-        $link .= '</a>';
-        return $link;
-    }
-
-    /**
-     * Get an icon representing the corrent sort state.
-     * @param $reverse sort is descending, not ascending.
-     * @return string HTML image tag.
-     */
-    protected function get_sort_icon($reverse) {
-        global $OUTPUT;
-        if ($reverse) {
-            return $OUTPUT->pix_icon('t/sort_desc', get_string('desc'), '', array('class' => 'iconsort'));
-        } else {
-            return $OUTPUT->pix_icon('t/sort_asc', get_string('asc'), '', array('class' => 'iconsort'));
-        }
-    }
-
-    /**
-     * Output this column.
-     * @param object $question the row from the $question table, augmented with extra information.
-     * @param string $rowclasses CSS class names that should be applied to this row of output.
-     */
-    public function display($question, $rowclasses) {
-        $this->display_start($question, $rowclasses);
-        $this->display_content($question, $rowclasses);
-        $this->display_end($question, $rowclasses);
-    }
-
-    /**
-     * Output the opening column tag.  If it is set as heading, it will use <th> tag instead of <td>
-     *
-     * @param stdClass $question
-     * @param array $rowclasses
-     */
-    protected function display_start($question, $rowclasses) {
-        $tag = 'td';
-        $attr = array('class' => $this->get_classes());
-        if ($this->isheading) {
-            $tag = 'th';
-            $attr['scope'] = 'row';
-        }
-        echo html_writer::start_tag($tag, $attr);
-    }
-
-    /**
-     * @return string the CSS classes to apply to every cell in this column.
-     */
-    protected function get_classes() {
-        $classes = $this->get_extra_classes();
-        $classes[] = $this->get_name();
-        return implode(' ', $classes);
-    }
-
-    /**
-     * @param object $question the row from the $question table, augmented with extra information.
-     * @return string internal name for this column. Used as a CSS class name,
-     *     and to store information about the current sort. Must match PARAM_ALPHA.
-     */
-    public abstract function get_name();
-
-    /**
-     * @return array any extra class names you would like applied to every cell in this column.
-     */
-    public function get_extra_classes() {
-        return array();
-    }
-
-    /**
-     * Output the contents of this column.
-     * @param object $question the row from the $question table, augmented with extra information.
-     * @param string $rowclasses CSS class names that should be applied to this row of output.
-     */
-    protected abstract function display_content($question, $rowclasses);
-
-    /**
-     * Output the closing column tag
-     *
-     * @param object $question
-     * @param string $rowclasses
-     */
-    protected function display_end($question, $rowclasses) {
-        $tag = 'td';
-        if ($this->isheading) {
-            $tag = 'th';
-        }
-        echo html_writer::end_tag($tag);
-    }
-
-    /**
-     * Return an array 'table_alias' => 'JOIN clause' to bring in any data that
-     * this column required.
-     *
-     * The return values for all the columns will be checked. It is OK if two
-     * columns join in the same table with the same alias and identical JOIN clauses.
-     * If to columns try to use the same alias with different joins, you get an error.
-     * The only table included by default is the question table, which is aliased to 'q'.
-     *
-     * It is importnat that your join simply adds additional data (or NULLs) to the
-     * existing rows of the query. It must not cause additional rows.
-     *
-     * @return array 'table_alias' => 'JOIN clause'
-     */
-    public function get_extra_joins() {
-        return array();
-    }
-
-    /**
-     * @return array fields required. use table alias 'q' for the question table, or one of the
-     * ones from get_extra_joins. Every field requested must specify a table prefix.
-     */
-    public function get_required_fields() {
-        return array();
-    }
-
-    /**
-     * Can this column be sorted on? You can return either:
-     *  + false for no (the default),
-     *  + a field name, if sorting this column corresponds to sorting on that datbase field.
-     *  + an array of subnames to sort on as follows
-     *  return array(
-     *      'firstname' => array('field' => 'uc.firstname', 'title' => get_string('firstname')),
-     *      'lastname' => array('field' => 'uc.lastname', 'field' => get_string('lastname')),
-     *  );
-     * As well as field, and field, you can also add 'revers' => 1 if you want the default sort
-     * order to be DESC.
-     * @return mixed as above.
-     */
-    public function is_sortable() {
-        return false;
-    }
-
-    /**
-     * Helper method for building sort clauses.
-     * @param bool $reverse whether the normal direction should be reversed.
-     * @param string $normaldir 'ASC' or 'DESC'
-     * @return string 'ASC' or 'DESC'
-     */
-    protected function sortorder($reverse) {
-        if ($reverse) {
-            return ' DESC';
-        } else {
-            return ' ASC';
-        }
-    }
-
-    /**
-     * @param $reverse Whether to sort in the reverse of the default sort order.
-     * @param $subsort if is_sortable returns an array of subnames, then this will be
-     *      one of those. Otherwise will be empty.
-     * @return string some SQL to go in the order by clause.
-     */
-    public function sort_expression($reverse, $subsort) {
-        $sortable = $this->is_sortable();
-        if (is_array($sortable)) {
-            if (array_key_exists($subsort, $sortable)) {
-                return $sortable[$subsort]['field'] . $this->sortorder($reverse, !empty($sortable[$subsort]['reverse']));
-            } else {
-                throw new coding_exception('Unexpected $subsort type: ' . $subsort);
-            }
-        } else if ($sortable) {
-            return $sortable . $this->sortorder($reverse);
-        } else {
-            throw new coding_exception('sort_expression called on a non-sortable column.');
-        }
-    }
-}
-
+class_alias('core_question\bank\column_base', 'question_bank_column_base', true);
 
 /**
  * A column with a checkbox for each question with name q{questionid}.
  *
  * @copyright  2009 Tim Hunt
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @deprecated since Moodle 2.7 MDL-40457
  */
-class question_bank_checkbox_column extends question_bank_column_base {
-    protected $strselect;
-    protected $firstrow = true;
-
-    public function init() {
-        $this->strselect = get_string('select');
-    }
-
-    public function get_name() {
-        return 'checkbox';
-    }
-
-    protected function get_title() {
-        return '<input type="checkbox" disabled="disabled" id="qbheadercheckbox" />';
-    }
-
-    protected function get_title_tip() {
-        return get_string('selectquestionsforbulk', 'question');
-    }
-
-    protected function display_content($question, $rowclasses) {
-        global $PAGE;
-        echo '<input title="' . $this->strselect . '" type="checkbox" name="q' .
-                $question->id . '" id="checkq' . $question->id . '" value="1"/>';
-        if ($this->firstrow) {
-            $PAGE->requires->strings_for_js(array('selectall', 'deselectall'), 'moodle');
-            $PAGE->requires->yui_module('moodle-question-qbankmanager', 'M.question.qbankmanager.init',
-                    array('checkq' . $question->id));
-            $this->firstrow = false;
-        }
-    }
-
-    public function get_required_fields() {
-        return array('q.id');
-    }
-}
-
+class_alias('core_question\bank\checkbox_column', 'question_bank_checkbox_column', true);
 
 /**
  * A column type for the name of the question type.
  *
  * @copyright  2009 Tim Hunt
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @deprecated since Moodle 2.7 MDL-40457
  */
-class question_bank_question_type_column extends question_bank_column_base {
-    public function get_name() {
-        return 'qtype';
-    }
-
-    protected function get_title() {
-        return get_string('qtypeveryshort', 'question');
-    }
-
-    protected function get_title_tip() {
-        return get_string('questiontype', 'question');
-    }
-
-    protected function display_content($question, $rowclasses) {
-        echo print_question_icon($question);
-    }
-
-    public function get_required_fields() {
-        return array('q.qtype');
-    }
-
-    public function is_sortable() {
-        return 'q.qtype';
-    }
-}
+class_alias('core_question\bank\question_type_column', 'question_bank_question_type_column', true);
 
 
 /**
@@ -492,48 +157,9 @@ class question_bank_question_type_column extends question_bank_column_base {
  *
  * @copyright  2009 Tim Hunt
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @deprecated since Moodle 2.7 MDL-40457
  */
-class question_bank_question_name_column extends question_bank_column_base {
-    protected $checkboxespresent = null;
-
-    public function get_name() {
-        return 'questionname';
-    }
-
-    protected function get_title() {
-        return get_string('question');
-    }
-
-    protected function label_for($question) {
-        if (is_null($this->checkboxespresent)) {
-            $this->checkboxespresent = $this->qbank->has_column('checkbox');
-        }
-        if ($this->checkboxespresent) {
-            return 'checkq' . $question->id;
-        } else {
-            return '';
-        }
-    }
-
-    protected function display_content($question, $rowclasses) {
-        $labelfor = $this->label_for($question);
-        if ($labelfor) {
-            echo '<label for="' . $labelfor . '">';
-        }
-        echo format_string($question->name);
-        if ($labelfor) {
-            echo '</label>';
-        }
-    }
-
-    public function get_required_fields() {
-        return array('q.id', 'q.name');
-    }
-
-    public function is_sortable() {
-        return 'q.name';
-    }
-}
+class_alias('core_question\bank\question_name_column', 'question_bank_question_name_column', true);
 
 
 /**
@@ -541,44 +167,9 @@ class question_bank_question_name_column extends question_bank_column_base {
  *
  * @copyright  2009 Tim Hunt
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @deprecated since Moodle 2.7 MDL-40457
  */
-class question_bank_creator_name_column extends question_bank_column_base {
-    public function get_name() {
-        return 'creatorname';
-    }
-
-    protected function get_title() {
-        return get_string('createdby', 'question');
-    }
-
-    protected function display_content($question, $rowclasses) {
-        if (!empty($question->creatorfirstname) && !empty($question->creatorlastname)) {
-            $u = new stdClass();
-            $u = username_load_fields_from_object($u, $question, 'creator');
-            echo fullname($u);
-        }
-    }
-
-    public function get_extra_joins() {
-        return array('uc' => 'LEFT JOIN {user} uc ON uc.id = q.createdby');
-    }
-
-    public function get_required_fields() {
-        $allnames = get_all_user_name_fields();
-        $requiredfields = array();
-        foreach ($allnames as $allname) {
-            $requiredfields[] = 'uc.' . $allname . ' AS creator' . $allname;
-        }
-        return $requiredfields;
-    }
-
-    public function is_sortable() {
-        return array(
-            'firstname' => array('field' => 'uc.firstname', 'title' => get_string('firstname')),
-            'lastname' => array('field' => 'uc.lastname', 'title' => get_string('lastname')),
-        );
-    }
-}
+class_alias('core_question\bank\creator_name_column', 'question_bank_creator_name_column', true);
 
 
 /**
@@ -586,44 +177,9 @@ class question_bank_creator_name_column extends question_bank_column_base {
  *
  * @copyright  2009 Tim Hunt
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @deprecated since Moodle 2.7 MDL-40457
  */
-class question_bank_modifier_name_column extends question_bank_column_base {
-    public function get_name() {
-        return 'modifiername';
-    }
-
-    protected function get_title() {
-        return get_string('lastmodifiedby', 'question');
-    }
-
-    protected function display_content($question, $rowclasses) {
-        if (!empty($question->modifierfirstname) && !empty($question->modifierlastname)) {
-            $u = new stdClass();
-            $u = username_load_fields_from_object($u, $question, 'modifier');
-            echo fullname($u);
-        }
-    }
-
-    public function get_extra_joins() {
-        return array('um' => 'LEFT JOIN {user} um ON um.id = q.modifiedby');
-    }
-
-    public function get_required_fields() {
-        $allnames = get_all_user_name_fields();
-        $requiredfields = array();
-        foreach ($allnames as $allname) {
-            $requiredfields[] = 'um.' . $allname . ' AS modifier' . $allname;
-        }
-        return $requiredfields;
-    }
-
-    public function is_sortable() {
-        return array(
-            'firstname' => array('field' => 'um.firstname', 'title' => get_string('firstname')),
-            'lastname' => array('field' => 'um.lastname', 'title' => get_string('lastname')),
-        );
-    }
-}
+class_alias('core_question\bank\modifier_name_column', 'question_bank_modifier_name_column', true);
 
 
 /**
@@ -631,28 +187,9 @@ class question_bank_modifier_name_column extends question_bank_column_base {
  *
  * @copyright  2009 Tim Hunt
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @deprecated since Moodle 2.7 MDL-40457
  */
-abstract class question_bank_action_column_base extends question_bank_column_base {
-
-    protected function get_title() {
-        return '&#160;';
-    }
-
-    public function get_extra_classes() {
-        return array('iconcol');
-    }
-
-    protected function print_icon($icon, $title, $url) {
-        global $OUTPUT;
-        echo '<a title="' . $title . '" href="' . $url . '">
-                <img src="' . $OUTPUT->pix_url($icon) . '" class="iconsmall" alt="' . $title . '" /></a>';
-    }
-
-    public function get_required_fields() {
-        // createdby is required for permission checks.
-        return array('q.id', 'q.createdby');
-    }
-}
+class_alias('core_question\bank\action_column_base', 'question_bank_action_column_base', true);
 
 
 /**
@@ -660,87 +197,27 @@ abstract class question_bank_action_column_base extends question_bank_column_bas
  *
  * @copyright  2009 Tim Hunt
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @deprecated since Moodle 2.7 MDL-40457
  */
-class question_bank_edit_action_column extends question_bank_action_column_base {
-    protected $stredit;
-    protected $strview;
-
-    public function init() {
-        parent::init();
-        $this->stredit = get_string('edit');
-        $this->strview = get_string('view');
-    }
-
-    public function get_name() {
-        return 'editaction';
-    }
-
-    protected function display_content($question, $rowclasses) {
-        if (question_has_capability_on($question, 'edit')) {
-            $this->print_icon('t/edit', $this->stredit, $this->qbank->edit_question_url($question->id));
-        } else if (question_has_capability_on($question, 'view')) {
-            $this->print_icon('i/info', $this->strview, $this->qbank->edit_question_url($question->id));
-        }
-    }
-}
+class_alias('core_question\bank\edit_action_column', 'question_bank_edit_action_column', true);
 
 /**
  * Question bank column for the duplicate action icon.
  *
  * @copyright  2013 The Open University
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @deprecated since Moodle 2.7 MDL-40457
  */
-class question_bank_copy_action_column extends question_bank_action_column_base {
-    /** @var string avoids repeated calls to get_string('duplicate'). */
-    protected $strcopy;
-
-    public function init() {
-        parent::init();
-        $this->strcopy = get_string('duplicate');
-    }
-
-    public function get_name() {
-        return 'copyaction';
-    }
-
-    protected function display_content($question, $rowclasses) {
-        // To copy a question, you need permission to add a question in the same
-        // category as the existing question, and ability to access the details of
-        // the question being copied.
-        if (question_has_capability_on($question, 'add') &&
-                (question_has_capability_on($question, 'edit') || question_has_capability_on($question, 'view'))) {
-            $this->print_icon('t/copy', $this->strcopy, $this->qbank->copy_question_url($question->id));
-        }
-    }
-}
+class_alias('core_question\bank\copy_action_column', 'question_bank_copy_action_column', true);
 
 /**
  * Question bank columns for the preview action icon.
  *
  * @copyright  2009 Tim Hunt
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @deprecated since Moodle 2.7 MDL-40457
  */
-class question_bank_preview_action_column extends question_bank_action_column_base {
-    public function init() {
-        parent::init();
-    }
-
-    public function get_name() {
-        return 'previewaction';
-    }
-
-    protected function display_content($question, $rowclasses) {
-        global $PAGE;
-        if (question_has_capability_on($question, 'use')) {
-            echo $PAGE->get_renderer('core_question')->question_preview_link(
-                    $question->id, $this->qbank->get_most_specific_context(), false);
-        }
-    }
-
-    public function get_required_fields() {
-        return array('q.id');
-    }
-}
+class_alias('core_question\bank\preview_action_column', 'question_bank_preview_action_column', true);
 
 
 /**
@@ -748,994 +225,34 @@ class question_bank_preview_action_column extends question_bank_action_column_ba
  *
  * @copyright  2009 Tim Hunt
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @deprecated since Moodle 2.7 MDL-40457
  */
-class question_bank_delete_action_column extends question_bank_action_column_base {
-    protected $strdelete;
-    protected $strrestore;
-
-    public function init() {
-        parent::init();
-        $this->strdelete = get_string('delete');
-        $this->strrestore = get_string('restore');
-    }
-
-    public function get_name() {
-        return 'deleteaction';
-    }
-
-    protected function display_content($question, $rowclasses) {
-        if (question_has_capability_on($question, 'edit')) {
-            if ($question->hidden) {
-                $url = new moodle_url($this->qbank->base_url(), array('unhide' => $question->id, 'sesskey'=>sesskey()));
-                $this->print_icon('t/restore', $this->strrestore, $url);
-            } else {
-                $url = new moodle_url($this->qbank->base_url(), array('deleteselected' => $question->id, 'q' . $question->id => 1, 'sesskey'=>sesskey()));
-                $this->print_icon('t/delete', $this->strdelete, $url);
-            }
-        }
-    }
-
-    public function get_required_fields() {
-        return array('q.id', 'q.hidden');
-    }
-}
+class_alias('core_question\bank\delete_action_column', 'question_bank_delete_action_column', true);
 
 /**
  * Base class for 'columns' that are actually displayed as a row following the main question row.
  *
  * @copyright  2009 Tim Hunt
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @deprecated since Moodle 2.7 MDL-40457
  */
-abstract class question_bank_row_base extends question_bank_column_base {
-    public function is_extra_row() {
-        return true;
-    }
-
-    protected function display_start($question, $rowclasses) {
-        if ($rowclasses) {
-            echo '<tr class="' . $rowclasses . '">' . "\n";
-        } else {
-            echo "<tr>\n";
-        }
-        echo '<td colspan="' . $this->qbank->get_column_count() . '" class="' . $this->get_name() . '">';
-    }
-
-    protected function display_end($question, $rowclasses) {
-        echo "</td></tr>\n";
-    }
-}
+class_alias('core_question\bank\row_base', 'question_bank_row_base', true);
 
 /**
  * A column type for the name of the question name.
  *
  * @copyright  2009 Tim Hunt
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @deprecated since Moodle 2.7 MDL-40457
  */
-class question_bank_question_text_row extends question_bank_row_base {
-    protected $formatoptions;
-
-    protected function init() {
-        $this->formatoptions = new stdClass();
-        $this->formatoptions->noclean = true;
-        $this->formatoptions->para = false;
-    }
-
-    public function get_name() {
-        return 'questiontext';
-    }
-
-    protected function get_title() {
-        return get_string('questiontext', 'question');
-    }
-
-    protected function display_content($question, $rowclasses) {
-        $text = question_rewrite_question_preview_urls($question->questiontext, $question->id,
-                $question->contextid, 'question', 'questiontext', $question->id,
-                $question->contextid, 'core_question');
-        $text = format_text($text, $question->questiontextformat,
-                $this->formatoptions);
-        if ($text == '') {
-            $text = '&#160;';
-        }
-        echo $text;
-    }
-
-    public function get_extra_joins() {
-        return array('qc' => 'JOIN {question_categories} qc ON qc.id = q.category');
-    }
-
-    public function get_required_fields() {
-        return array('q.id', 'q.questiontext', 'q.questiontextformat', 'qc.contextid');
-    }
-}
+class_alias('core_question\bank\question_text_row', 'question_bank_question_text_row', true);
 
 /**
- * This class prints a view of the question bank, including
- *  + Some controls to allow users to to select what is displayed.
- *  + A list of questions as a table.
- *  + Further controls to do things with the questions.
- *
- * This class gives a basic view, and provides plenty of hooks where subclasses
- * can override parts of the display.
- *
- * The list of questions presented as a table is generated by creating a list of
- * question_bank_column objects, one for each 'column' to be displayed. These
- * manage
- *  + outputting the contents of that column, given a $question object, but also
- *  + generating the right fragments of SQL to ensure the necessary data is present,
- *    and sorted in the right order.
- *  + outputting table headers.
- *
  * @copyright  2009 Tim Hunt
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @deprecated since Moodle 2.7 MDL-40457
  */
-class question_bank_view {
-    const MAX_SORTS = 3;
-
-    protected $baseurl;
-    protected $editquestionurl;
-    protected $quizorcourseid;
-    protected $contexts;
-    protected $cm;
-    protected $course;
-    protected $knowncolumntypes;
-    protected $visiblecolumns;
-    protected $extrarows;
-    protected $requiredcolumns;
-    protected $sort;
-    protected $lastchangedid;
-    protected $countsql;
-    protected $loadsql;
-    protected $sqlparams;
-    /** @var array of \core_question\bank\search\condition objects. */
-    protected $searchconditions = array();
-
-    /**
-     * Constructor
-     * @param question_edit_contexts $contexts
-     * @param moodle_url $pageurl
-     * @param object $course course settings
-     * @param object $cm (optional) activity settings.
-     */
-    public function __construct($contexts, $pageurl, $course, $cm = null) {
-        global $CFG, $PAGE;
-
-        $this->contexts = $contexts;
-        $this->baseurl = $pageurl;
-        $this->course = $course;
-        $this->cm = $cm;
-
-        if (!empty($cm) && $cm->modname == 'quiz') {
-            $this->quizorcourseid = '&amp;quizid=' . $cm->instance;
-        } else {
-            $this->quizorcourseid = '&amp;courseid=' .$this->course->id;
-        }
-
-        // Create the url of the new question page to forward to.
-        $returnurl = $pageurl->out_as_local_url(false);
-        $this->editquestionurl = new moodle_url('/question/question.php',
-                array('returnurl' => $returnurl));
-        if ($cm !== null){
-            $this->editquestionurl->param('cmid', $cm->id);
-        } else {
-            $this->editquestionurl->param('courseid', $this->course->id);
-        }
-
-        $this->lastchangedid = optional_param('lastchanged',0,PARAM_INT);
-
-        $this->init_column_types();
-        $this->init_columns($this->wanted_columns(), $this->heading_column());
-        $this->init_sort();
-        $this->init_search_conditions($this->contexts, $this->course, $this->cm);
-    }
-
-    /**
-     * Initialize search conditions from plugins
-     * local_*_get_question_bank_search_conditions() must return an array of
-     * \core_question\bank\search\condition objects.
-     */
-    protected function init_search_conditions() {
-        $searchplugins = get_plugin_list_with_function('local', 'get_question_bank_search_conditions');
-        foreach ($searchplugins as $component => $function) {
-            foreach ($function($this) as $searchobject) {
-                $this->add_searchcondition($searchobject);
-            }
-        }
-    }
-
-    protected function wanted_columns() {
-        $columns = array('checkbox', 'qtype', 'questionname', 'editaction', 'copyaction',
-                        'previewaction', 'deleteaction', 'creatorname', 'modifiername');
-        if (question_get_display_preference('qbshowtext', 0, PARAM_BOOL, new moodle_url(''))) {
-            $columns[] = 'questiontext';
-        }
-        return $columns;
-    }
-
-    /**
-     * Specify the column heading
-     *
-     * @return string Column name for the heading
-     */
-    protected function heading_column() {
-        return 'questionname';
-    }
-
-    protected function known_field_types() {
-        return array(
-            new question_bank_checkbox_column($this),
-            new question_bank_question_type_column($this),
-            new question_bank_question_name_column($this),
-            new question_bank_creator_name_column($this),
-            new question_bank_modifier_name_column($this),
-            new question_bank_edit_action_column($this),
-            new question_bank_copy_action_column($this),
-            new question_bank_preview_action_column($this),
-            new question_bank_delete_action_column($this),
-            new question_bank_question_text_row($this),
-        );
-    }
-
-    protected function init_column_types() {
-        $this->knowncolumntypes = array();
-        foreach ($this->known_field_types() as $col) {
-            $this->knowncolumntypes[$col->get_name()] = $col;
-        }
-    }
-
-    /**
-     * Initializing table columns
-     *
-     * @param array $wanted Collection of column names
-     * @param string $heading The name of column that is set as heading
-     */
-    protected function init_columns($wanted, $heading = '') {
-        $this->visiblecolumns = array();
-        $this->extrarows = array();
-        foreach ($wanted as $colname) {
-            if (!isset($this->knowncolumntypes[$colname])) {
-                throw new coding_exception('Unknown column type ' . $colname . ' requested in init columns.');
-            }
-            $column = $this->knowncolumntypes[$colname];
-            if ($column->is_extra_row()) {
-                $this->extrarows[$colname] = $column;
-            } else {
-                $this->visiblecolumns[$colname] = $column;
-            }
-        }
-        $this->requiredcolumns = array_merge($this->visiblecolumns, $this->extrarows);
-        if (array_key_exists($heading, $this->requiredcolumns)) {
-            $this->requiredcolumns[$heading]->set_as_heading();
-        }
-    }
-
-    /**
-     * @param string $colname a column internal name.
-     * @return bool is this column included in the output?
-     */
-    public function has_column($colname) {
-        return isset($this->visiblecolumns[$colname]);
-    }
-
-    /**
-     * @return int The number of columns in the table.
-     */
-    public function get_column_count() {
-        return count($this->visiblecolumns);
-    }
-
-    public function get_courseid() {
-        return $this->course->id;
-    }
-
-    protected function init_sort() {
-        $this->init_sort_from_params();
-        if (empty($this->sort)) {
-            $this->sort = $this->default_sort();
-        }
-    }
-
-    /**
-     * Deal with a sort name of the form columnname, or colname_subsort by
-     * breaking it up, validating the bits that are presend, and returning them.
-     * If there is no subsort, then $subsort is returned as ''.
-     * @return array array($colname, $subsort).
-     */
-    protected function parse_subsort($sort) {
-    /// Do the parsing.
-        if (strpos($sort, '_') !== false) {
-            list($colname, $subsort) = explode('_', $sort, 2);
-        } else {
-            $colname = $sort;
-            $subsort = '';
-        }
-    /// Validate the column name.
-        if (!isset($this->knowncolumntypes[$colname]) || !$this->knowncolumntypes[$colname]->is_sortable()) {
-            for ($i = 1; $i <= question_bank_view::MAX_SORTS; $i++) {
-                $this->baseurl->remove_params('qbs' . $i);
-            }
-            throw new moodle_exception('unknownsortcolumn', '', $link = $this->baseurl->out(), $colname);
-        }
-    /// Validate the subsort, if present.
-        if ($subsort) {
-            $subsorts = $this->knowncolumntypes[$colname]->is_sortable();
-            if (!is_array($subsorts) || !isset($subsorts[$subsort])) {
-                throw new moodle_exception('unknownsortcolumn', '', $link = $this->baseurl->out(), $sort);
-            }
-        }
-        return array($colname, $subsort);
-    }
-
-    protected function init_sort_from_params() {
-        $this->sort = array();
-        for ($i = 1; $i <= question_bank_view::MAX_SORTS; $i++) {
-            if (!$sort = optional_param('qbs' . $i, '', PARAM_ALPHAEXT)) {
-                break;
-            }
-            // Work out the appropriate order.
-            $order = 1;
-            if ($sort[0] == '-') {
-                $order = -1;
-                $sort = substr($sort, 1);
-                if (!$sort) {
-                    break;
-                }
-            }
-            // Deal with subsorts.
-            list($colname, $subsort) = $this->parse_subsort($sort);
-            $this->requiredcolumns[$colname] = $this->knowncolumntypes[$colname];
-            $this->sort[$sort] = $order;
-        }
-    }
-
-    protected function sort_to_params($sorts) {
-        $params = array();
-        $i = 0;
-        foreach ($sorts as $sort => $order) {
-            $i += 1;
-            if ($order < 0) {
-                $sort = '-' . $sort;
-            }
-            $params['qbs' . $i] = $sort;
-        }
-        return $params;
-    }
-
-    protected function default_sort() {
-        $this->requiredcolumns['qtype'] = $this->knowncolumntypes['qtype'];
-        $this->requiredcolumns['questionname'] = $this->knowncolumntypes['questionname'];
-        return array('qtype' => 1, 'questionname' => 1);
-    }
-
-    /**
-     * @param $sort a column or column_subsort name.
-     * @return int the current sort order for this column -1, 0, 1
-     */
-    public function get_primary_sort_order($sort) {
-        $order = reset($this->sort);
-        $primarysort = key($this->sort);
-        if ($sort == $primarysort) {
-            return $order;
-        } else {
-            return 0;
-        }
-    }
-
-    /**
-     * Get a URL to redisplay the page with a new sort for the question bank.
-     * @param string $sort the column, or column_subsort to sort on.
-     * @param bool $newsortreverse whether to sort in reverse order.
-     * @return string The new URL.
-     */
-    public function new_sort_url($sort, $newsortreverse) {
-        if ($newsortreverse) {
-            $order = -1;
-        } else {
-            $order = 1;
-        }
-        // Tricky code to add the new sort at the start, removing it from where it was before, if it was present.
-        $newsort = array_reverse($this->sort);
-        if (isset($newsort[$sort])) {
-            unset($newsort[$sort]);
-        }
-        $newsort[$sort] = $order;
-        $newsort = array_reverse($newsort);
-        if (count($newsort) > question_bank_view::MAX_SORTS) {
-            $newsort = array_slice($newsort, 0, question_bank_view::MAX_SORTS, true);
-        }
-        return $this->baseurl->out(true, $this->sort_to_params($newsort));
-    }
-
-    /**
-     * Create the SQL query to retrieve the indicated questions
-     * @param stdClass $category no longer used.
-     * @param bool $recurse no longer used.
-     * @param bool $showhidden no longer used.
-     * @deprecated since Moodle 2.7 MDL-40313.
-     * @see build_query()
-     * @see \core_question\bank\search\condition
-     * @todo MDL-41978 This will be deleted in Moodle 2.8
-     */
-    protected function build_query_sql($category, $recurse, $showhidden) {
-        debugging('build_query_sql() is deprecated, please use question_bank_view::build_query() and ' .
-                '\core_question\bank\search\condition classes instead.', DEBUG_DEVELOPER);
-        self::build_query();
-    }
-
-    /**
-     * Create the SQL query to retrieve the indicated questions, based on
-     * \core_question\bank\search\condition filters.
-     */
-    protected function build_query() {
-        global $DB;
-
-    /// Get the required tables.
-        $joins = array();
-        foreach ($this->requiredcolumns as $column) {
-            $extrajoins = $column->get_extra_joins();
-            foreach ($extrajoins as $prefix => $join) {
-                if (isset($joins[$prefix]) && $joins[$prefix] != $join) {
-                    throw new coding_exception('Join ' . $join . ' conflicts with previous join ' . $joins[$prefix]);
-                }
-                $joins[$prefix] = $join;
-            }
-        }
-
-    /// Get the required fields.
-        $fields = array('q.hidden', 'q.category');
-        foreach ($this->visiblecolumns as $column) {
-            $fields = array_merge($fields, $column->get_required_fields());
-        }
-        foreach ($this->extrarows as $row) {
-            $fields = array_merge($fields, $row->get_required_fields());
-        }
-        $fields = array_unique($fields);
-
-    /// Build the order by clause.
-        $sorts = array();
-        foreach ($this->sort as $sort => $order) {
-            list($colname, $subsort) = $this->parse_subsort($sort);
-            $sorts[] = $this->requiredcolumns[$colname]->sort_expression($order < 0, $subsort);
-        }
-
-    /// Build the where clause.
-        $tests = array('q.parent = 0');
-        $this->sqlparams = array();
-        foreach ($this->searchconditions as $searchcondition) {
-            if ($searchcondition->where()) {
-                $tests[] = '((' . $searchcondition->where() .'))';
-            }
-            if ($searchcondition->params()) {
-                $this->sqlparams = array_merge($this->sqlparams, $searchcondition->params());
-            }
-        }
-    /// Build the SQL.
-        $sql = ' FROM {question} q ' . implode(' ', $joins);
-        $sql .= ' WHERE ' . implode(' AND ', $tests);
-        $this->countsql = 'SELECT count(1)' . $sql;
-        $this->loadsql = 'SELECT ' . implode(', ', $fields) . $sql . ' ORDER BY ' . implode(', ', $sorts);
-    }
-
-    protected function get_question_count() {
-        global $DB;
-        return $DB->count_records_sql($this->countsql, $this->sqlparams);
-    }
-
-    protected function load_page_questions($page, $perpage) {
-        global $DB;
-        $questions = $DB->get_recordset_sql($this->loadsql, $this->sqlparams, $page*$perpage, $perpage);
-        if (!$questions->valid()) {
-        /// No questions on this page. Reset to page 0.
-            $questions = $DB->get_recordset_sql($this->loadsql, $this->sqlparams, 0, $perpage);
-        }
-        return $questions;
-    }
-
-    public function base_url() {
-        return $this->baseurl;
-    }
-
-    public function edit_question_url($questionid) {
-        return $this->editquestionurl->out(true, array('id' => $questionid));
-    }
-
-    /**
-     * Get the URL for duplicating a given question.
-     * @param int $questionid the question id.
-     * @return moodle_url the URL.
-     */
-    public function copy_question_url($questionid) {
-        return $this->editquestionurl->out(true, array('id' => $questionid, 'makecopy' => 1));
-    }
-
-    /**
-     * Get the context we are displaying the question bank for.
-     * @return context context object.
-     */
-    public function get_most_specific_context() {
-        return $this->contexts->lowest();
-    }
-
-    /**
-     * Get the URL to preview a question.
-     * @param stdClass $questiondata the data defining the question.
-     * @return moodle_url the URL.
-     */
-    public function preview_question_url($questiondata) {
-        return question_preview_url($questiondata->id, null, null, null, null,
-                $this->get_most_specific_context());
-    }
-
-    /**
-     * Shows the question bank editing interface.
-     *
-     * The function also processes a number of actions:
-     *
-     * Actions affecting the question pool:
-     * move           Moves a question to a different category
-     * deleteselected Deletes the selected questions from the category
-     * Other actions:
-     * category      Chooses the category
-     * displayoptions Sets display options
-     */
-    public function display($tabname, $page, $perpage, $cat,
-            $recurse, $showhidden, $showquestiontext) {
-        global $PAGE, $OUTPUT;
-
-        if ($this->process_actions_needing_ui()) {
-            return;
-        }
-        $editcontexts = $this->contexts->having_one_edit_tab_cap($tabname);
-        // Category selection form
-        echo $OUTPUT->heading(get_string('questionbank', 'question'), 2);
-        array_unshift($this->searchconditions, new \core_question\bank\search\hidden_condition(!$showhidden));
-        array_unshift($this->searchconditions, new \core_question\bank\search\category_condition(
-                $cat, $recurse, $editcontexts, $this->baseurl, $this->course));
-        $this->display_options_form($showquestiontext);
-
-        // continues with list of questions
-        $this->display_question_list($this->contexts->having_one_edit_tab_cap($tabname),
-                $this->baseurl, $cat, $this->cm,
-                null, $page, $perpage, $showhidden, $showquestiontext,
-                $this->contexts->having_cap('moodle/question:add'));
-    }
-
-    protected function print_choose_category_message($categoryandcontext) {
-        echo "<p style=\"text-align:center;\"><b>";
-        print_string('selectcategoryabove', 'question');
-        echo "</b></p>";
-    }
-
-    protected function get_current_category($categoryandcontext) {
-        global $DB, $OUTPUT;
-        list($categoryid, $contextid) = explode(',', $categoryandcontext);
-        if (!$categoryid) {
-            $this->print_choose_category_message($categoryandcontext);
-            return false;
-        }
-
-        if (!$category = $DB->get_record('question_categories',
-                array('id' => $categoryid, 'contextid' => $contextid))) {
-            echo $OUTPUT->box_start('generalbox questionbank');
-            echo $OUTPUT->notification('Category not found!');
-            echo $OUTPUT->box_end();
-            return false;
-        }
-
-        return $category;
-    }
-
-    /**
-     * prints category information
-     * @param stdClass $category the category row from the database.
-     * @deprecated since Moodle 2.7 MDL-40313.
-     * @see \core_question\bank\search\condition
-     * @todo MDL-41978 This will be deleted in Moodle 2.8
-     */
-    protected function print_category_info($category) {
-        $formatoptions = new stdClass();
-        $formatoptions->noclean = true;
-        $formatoptions->overflowdiv = true;
-        echo '<div class="boxaligncenter">';
-        echo format_text($category->info, $category->infoformat, $formatoptions, $this->course->id);
-        echo "</div>\n";
-    }
-
-    /**
-     * Prints a form to choose categories
-     * @deprecated since Moodle 2.7 MDL-40313.
-     * @see \core_question\bank\search\condition
-     * @todo MDL-41978 This will be deleted in Moodle 2.8
-     */
-    protected function display_category_form($contexts, $pageurl, $current) {
-        global $OUTPUT;
-
-        debugging('display_category_form() is deprecated, please use ' .
-                '\core_question\bank\search\condition instead.', DEBUG_DEVELOPER);
-    /// Get all the existing categories now
-        echo '<div class="choosecategory">';
-        $catmenu = question_category_options($contexts, false, 0, true);
-
-        $select = new single_select($this->baseurl, 'category', $catmenu, $current, null, 'catmenu');
-        $select->set_label(get_string('selectacategory', 'question'));
-        echo $OUTPUT->render($select);
-        echo "</div>\n";
-    }
-
-    /**
-     * Display the options form.
-     * @param bool $recurse no longer used.
-     * @param bool $showhidden no longer used.
-     * @param bool $showquestiontext whether to show the question text.
-     * @deprecated since Moodle 2.7 MDL-40313.
-     * @see display_options_form
-     * @todo MDL-41978 This will be deleted in Moodle 2.8
-     * @see \core_question\bank\search\condition
-     */
-    protected function display_options($recurse, $showhidden, $showquestiontext) {
-        debugging('display_options() is deprecated, please use display_options_form instead.', DEBUG_DEVELOPER);
-        return $this->display_options_form($showquestiontext);
-    }
-
-    /**
-     * Print a single option checkbox.
-     * @deprecated since Moodle 2.7 MDL-40313.
-     * @see \core_question\bank\search\condition
-     * @see html_writer::checkbox
-     * @todo MDL-41978 This will be deleted in Moodle 2.8
-     */
-    protected function display_category_form_checkbox($name, $value, $label) {
-        debugging('display_category_form_checkbox() is deprecated, ' .
-                'please use \core_question\bank\search\condition instead.', DEBUG_DEVELOPER);
-        echo '<div><input type="hidden" id="' . $name . '_off" name="' . $name . '" value="0" />';
-        echo '<input type="checkbox" id="' . $name . '_on" name="' . $name . '" value="1"';
-        if ($value) {
-            echo ' checked="checked"';
-        }
-        echo ' onchange="getElementById(\'displayoptions\').submit(); return true;" />';
-        echo '<label for="' . $name . '_on">' . $label . '</label>';
-        echo "</div>\n";
-    }
-
-    /**
-     * Display the form with options for which questions are displayed and how they are displayed.
-     * @param bool $showquestiontext Display the text of the question within the list.
-     */
-    protected function display_options_form($showquestiontext) {
-        global $PAGE;
-
-        echo '<form method="get" action="edit.php" id="displayoptions">';
-        echo "<fieldset class='invisiblefieldset'>";
-        echo html_writer::input_hidden_params($this->baseurl, array('recurse', 'showhidden', 'qbshowtext'));
-
-        foreach ($this->searchconditions as $searchcondition) {
-            echo $searchcondition->display_options($this);
-        }
-        $this->display_showtext_checkbox($showquestiontext);
-        $this->display_advanced_search_form();
-        $PAGE->requires->yui_module('moodle-question-searchform', 'M.question.searchform.init');
-        echo '<noscript><div class="centerpara"><input type="submit" value="'. get_string('go') .'" />';
-        echo '</div></noscript></fieldset></form>';
-    }
-
-    /**
-     * Print the "advanced" UI elements for the form to select which questions. Hidden by default.
-     */
-    protected function display_advanced_search_form() {
-        print_collapsible_region_start('', 'advancedsearch', get_string('advancedsearchoptions', 'question'),
-                                               'question_bank_advanced_search');
-        foreach ($this->searchconditions as $searchcondition) {
-            echo $searchcondition->display_options_adv($this);
-        }
-        print_collapsible_region_end();
-    }
-
-    /**
-     * Display the checkbox UI for toggling the display of the question text in the list.
-     * @param bool $showquestiontext the current or default value for whether to display the text.
-     */
-    protected function display_showtext_checkbox($showquestiontext) {
-        echo '<div>';
-        echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'qbshowtext',
-                                               'value' => 0, 'id' => 'qbshowtext_off'));
-        echo html_writer::checkbox('qbshowtext', '1', $showquestiontext, get_string('showquestiontext', 'question'),
-                                       array('id' => 'qbshowtext_on', 'class' => 'searchoptions'));
-        echo "</div>\n";
-    }
-
-    protected function create_new_question_form($category, $canadd) {
-        global $CFG;
-        echo '<div class="createnewquestion">';
-        if ($canadd) {
-            create_new_question_button($category->id, $this->editquestionurl->params(),
-                    get_string('createnewquestion', 'question'));
-        } else {
-            print_string('nopermissionadd', 'question');
-        }
-        echo '</div>';
-    }
-
-    /**
-     * Prints the table of questions in a category with interactions
-     *
-     * @param array      $contexts    Not used!
-     * @param moodle_url $pageurl     The URL to reload this page.
-     * @param string     $categoryandcontext 'categoryID,contextID'.
-     * @param stdClass   $cm          Not used!
-     * @param bool       $recurse     Whether to include subcategories.
-     * @param int        $page        The number of the page to be displayed
-     * @param int        $perpage     Number of questions to show per page
-     * @param bool       $showhidden  whether deleted questions should be displayed.
-     * @param bool       $showquestiontext whether the text of each question should be shown in the list. Deprecated.
-     * @param array      $addcontexts contexts where the user is allowed to add new questions.
-     */
-    protected function display_question_list($contexts, $pageurl, $categoryandcontext,
-            $cm = null, $recurse=1, $page=0, $perpage=100, $showhidden=false,
-            $showquestiontext = false, $addcontexts = array()) {
-        global $CFG, $DB, $OUTPUT;
-
-        $category = $this->get_current_category($categoryandcontext);
-
-        $cmoptions = new stdClass();
-        $cmoptions->hasattempts = !empty($this->quizhasattempts);
-
-        $strselectall = get_string('selectall');
-        $strselectnone = get_string('deselectall');
-        $strdelete = get_string('delete');
-
-        list($categoryid, $contextid) = explode(',', $categoryandcontext);
-        $catcontext = context::instance_by_id($contextid);
-
-        $canadd = has_capability('moodle/question:add', $catcontext);
-        $caneditall =has_capability('moodle/question:editall', $catcontext);
-        $canuseall =has_capability('moodle/question:useall', $catcontext);
-        $canmoveall =has_capability('moodle/question:moveall', $catcontext);
-
-        $this->create_new_question_form($category, $canadd);
-
-        $this->build_query();
-        $totalnumber = $this->get_question_count();
-        if ($totalnumber == 0) {
-            return;
-        }
-        $questions = $this->load_page_questions($page, $perpage);
-
-        echo '<div class="categorypagingbarcontainer">';
-        $pageing_url = new moodle_url('edit.php');
-        $r = $pageing_url->params($pageurl->params());
-        $pagingbar = new paging_bar($totalnumber, $page, $perpage, $pageing_url);
-        $pagingbar->pagevar = 'qpage';
-        echo $OUTPUT->render($pagingbar);
-        echo '</div>';
-
-        echo '<form method="post" action="edit.php">';
-        echo '<fieldset class="invisiblefieldset" style="display: block;">';
-        echo '<input type="hidden" name="sesskey" value="'.sesskey().'" />';
-        echo html_writer::input_hidden_params($this->baseurl);
-
-        echo '<div class="categoryquestionscontainer">';
-        $this->start_table();
-        $rowcount = 0;
-        foreach ($questions as $question) {
-            $this->print_table_row($question, $rowcount);
-            $rowcount += 1;
-        }
-        $this->end_table();
-        echo "</div>\n";
-
-        echo '<div class="categorypagingbarcontainer pagingbottom">';
-        echo $OUTPUT->render($pagingbar);
-        if ($totalnumber > DEFAULT_QUESTIONS_PER_PAGE) {
-            if ($perpage == DEFAULT_QUESTIONS_PER_PAGE) {
-                $url = new moodle_url('edit.php', array_merge($pageurl->params(), array('qperpage'=>1000)));
-                $showall = '<a href="'.$url.'">'.get_string('showall', 'moodle', $totalnumber).'</a>';
-            } else {
-                $url = new moodle_url('edit.php', array_merge($pageurl->params(), array('qperpage'=>DEFAULT_QUESTIONS_PER_PAGE)));
-                $showall = '<a href="'.$url.'">'.get_string('showperpage', 'moodle', DEFAULT_QUESTIONS_PER_PAGE).'</a>';
-            }
-            echo "<div class='paging'>$showall</div>";
-        }
-        echo '</div>';
-
-        echo '<div class="modulespecificbuttonscontainer">';
-        if ($caneditall || $canmoveall || $canuseall){
-            echo '<strong>&nbsp;'.get_string('withselected', 'question').':</strong><br />';
-
-            if (function_exists('module_specific_buttons')) {
-                echo module_specific_buttons($this->cm->id,$cmoptions);
-            }
-
-            // print delete and move selected question
-            if ($caneditall) {
-                echo '<input type="submit" name="deleteselected" value="' . $strdelete . "\" />\n";
-            }
-
-            if ($canmoveall && count($addcontexts)) {
-                echo '<input type="submit" name="move" value="'.get_string('moveto', 'question')."\" />\n";
-                question_category_select_menu($addcontexts, false, 0, "$category->id,$category->contextid");
-            }
-
-            if (function_exists('module_specific_controls') && $canuseall) {
-                $modulespecific = module_specific_controls($totalnumber, $recurse, $category, $this->cm->id,$cmoptions);
-                if(!empty($modulespecific)){
-                    echo "<hr />$modulespecific";
-                }
-            }
-        }
-        echo "</div>\n";
-
-        echo '</fieldset>';
-        echo "</form>\n";
-    }
-
-    protected function start_table() {
-        echo '<table id="categoryquestions">' . "\n";
-        echo "<thead>\n";
-        $this->print_table_headers();
-        echo "</thead>\n";
-        echo "<tbody>\n";
-    }
-
-    protected function end_table() {
-        echo "</tbody>\n";
-        echo "</table>\n";
-    }
-
-    protected function print_table_headers() {
-        echo "<tr>\n";
-        foreach ($this->visiblecolumns as $column) {
-            $column->display_header();
-        }
-        echo "</tr>\n";
-    }
-
-    protected function get_row_classes($question, $rowcount) {
-        $classes = array();
-        if ($question->hidden) {
-            $classes[] = 'dimmed_text';
-        }
-        if ($question->id == $this->lastchangedid) {
-            $classes[] ='highlight';
-        }
-        $classes[] = 'r' . ($rowcount % 2);
-        return $classes;
-    }
-
-    protected function print_table_row($question, $rowcount) {
-        $rowclasses = implode(' ', $this->get_row_classes($question, $rowcount));
-        if ($rowclasses) {
-            echo '<tr class="' . $rowclasses . '">' . "\n";
-        } else {
-            echo "<tr>\n";
-        }
-        foreach ($this->visiblecolumns as $column) {
-            $column->display($question, $rowclasses);
-        }
-        echo "</tr>\n";
-        foreach ($this->extrarows as $row) {
-            $row->display($question, $rowclasses);
-        }
-    }
-
-    public function process_actions() {
-        global $CFG, $DB;
-        /// Now, check for commands on this page and modify variables as necessary
-        if (optional_param('move', false, PARAM_BOOL) and confirm_sesskey()) {
-            // Move selected questions to new category
-            $category = required_param('category', PARAM_SEQUENCE);
-            list($tocategoryid, $contextid) = explode(',', $category);
-            if (! $tocategory = $DB->get_record('question_categories', array('id' => $tocategoryid, 'contextid' => $contextid))) {
-                print_error('cannotfindcate', 'question');
-            }
-            $tocontext = context::instance_by_id($contextid);
-            require_capability('moodle/question:add', $tocontext);
-            $rawdata = (array) data_submitted();
-            $questionids = array();
-            foreach ($rawdata as $key => $value) {    // Parse input for question ids
-                if (preg_match('!^q([0-9]+)$!', $key, $matches)) {
-                    $key = $matches[1];
-                    $questionids[] = $key;
-                }
-            }
-            if ($questionids) {
-                list($usql, $params) = $DB->get_in_or_equal($questionids);
-                $sql = "";
-                $questions = $DB->get_records_sql("
-                        SELECT q.*, c.contextid
-                        FROM {question} q
-                        JOIN {question_categories} c ON c.id = q.category
-                        WHERE q.id $usql", $params);
-                foreach ($questions as $question){
-                    question_require_capability_on($question, 'move');
-                }
-                question_move_questions_to_category($questionids, $tocategory->id);
-                redirect($this->baseurl->out(false,
-                        array('category' => "$tocategoryid,$contextid")));
-            }
-        }
-
-        if (optional_param('deleteselected', false, PARAM_BOOL)) { // delete selected questions from the category
-            if (($confirm = optional_param('confirm', '', PARAM_ALPHANUM)) and confirm_sesskey()) { // teacher has already confirmed the action
-                $deleteselected = required_param('deleteselected', PARAM_RAW);
-                if ($confirm == md5($deleteselected)) {
-                    if ($questionlist = explode(',', $deleteselected)) {
-                        // for each question either hide it if it is in use or delete it
-                        foreach ($questionlist as $questionid) {
-                            $questionid = (int)$questionid;
-                            question_require_capability_on($questionid, 'edit');
-                            if (questions_in_use(array($questionid))) {
-                                $DB->set_field('question', 'hidden', 1, array('id' => $questionid));
-                            } else {
-                                question_delete_question($questionid);
-                            }
-                        }
-                    }
-                    redirect($this->baseurl);
-                } else {
-                    print_error('invalidconfirm', 'question');
-                }
-            }
-        }
-
-        // Unhide a question
-        if(($unhide = optional_param('unhide', '', PARAM_INT)) and confirm_sesskey()) {
-            question_require_capability_on($unhide, 'edit');
-            $DB->set_field('question', 'hidden', 0, array('id' => $unhide));
-
-            // Purge these questions from the cache.
-            question_bank::notify_question_edited($unhide);
-
-            redirect($this->baseurl);
-        }
-    }
-
-    public function process_actions_needing_ui() {
-        global $DB, $OUTPUT;
-        if (optional_param('deleteselected', false, PARAM_BOOL)) {
-            // make a list of all the questions that are selected
-            $rawquestions = $_REQUEST; // This code is called by both POST forms and GET links, so cannot use data_submitted.
-            $questionlist = '';  // comma separated list of ids of questions to be deleted
-            $questionnames = ''; // string with names of questions separated by <br /> with
-                                 // an asterix in front of those that are in use
-            $inuse = false;      // set to true if at least one of the questions is in use
-            foreach ($rawquestions as $key => $value) {    // Parse input for question ids
-                if (preg_match('!^q([0-9]+)$!', $key, $matches)) {
-                    $key = $matches[1];
-                    $questionlist .= $key.',';
-                    question_require_capability_on($key, 'edit');
-                    if (questions_in_use(array($key))) {
-                        $questionnames .= '* ';
-                        $inuse = true;
-                    }
-                    $questionnames .= $DB->get_field('question', 'name', array('id' => $key)) . '<br />';
-                }
-            }
-            if (!$questionlist) { // no questions were selected
-                redirect($this->baseurl);
-            }
-            $questionlist = rtrim($questionlist, ',');
-
-            // Add an explanation about questions in use
-            if ($inuse) {
-                $questionnames .= '<br />'.get_string('questionsinuse', 'question');
-            }
-            $baseurl = new moodle_url('edit.php', $this->baseurl->params());
-            $deleteurl = new moodle_url($baseurl, array('deleteselected'=>$questionlist, 'confirm'=>md5($questionlist), 'sesskey'=>sesskey()));
-
-            echo $OUTPUT->confirm(get_string('deletequestionscheck', 'question', $questionnames), $deleteurl, $baseurl);
-
-            return true;
-        }
-    }
-
-    /**
-     * Add another search control to this view.
-     * @param \core_question\bank\search\condition $searchcondition the condition to add.
-     */
-    public function add_searchcondition($searchcondition) {
-        $this->searchconditions[] = $searchcondition;
-    }
-}
+class_alias('core_question\bank\view', 'question_bank_view', true);
 
 /**
  * Common setup for all pages for editing questions.
@@ -1819,7 +336,7 @@ function question_edit_setup($edittab, $baseurl, $requirecmid = false, $requirec
 
     for ($i = 1; $i <= question_bank_view::MAX_SORTS; $i++) {
         $param = 'qbs' . $i;
-        if (!$sort = optional_param($param, '', PARAM_ALPHAEXT)) {
+        if (!$sort = optional_param($param, '', PARAM_TEXT)) {
             break;
         }
         $thispageurl->param($param, $sort);
index f755b38..4abcef0 100644 (file)
@@ -17,6 +17,14 @@ This files describes API changes for code that uses the question API.
 
 To add filters, local plugins can now implement the function local_[pluginname]_get_question_bank_search_conditions,
 
+2) To make columns available to question_bank_view, plugins can extend core_question\bank\column_base.
+   Users may choose to display additional columns by setting $CFG->questionbankcolumns to a comma-delimited list of columns.
+
+3) The subsort separator has changed from _ to - in order to distinuguish subsorts vs frankenstyle component separators.
+
+4) Because of the move to autoloading, $knowncolumntypes and known_field_types() are no longer used.
+
+5) question_bank_column_base and it's derived classes have been namespaced to core_question\bank\column_base.
 
 === 2.6 ===