]> git.mjollnir.org Git - moodle.git/commitdiff
question bank: MDL-17871 refactor the code that displays particular information about...
authortjhunt <tjhunt>
Wed, 21 Jan 2009 07:21:43 +0000 (07:21 +0000)
committertjhunt <tjhunt>
Wed, 21 Jan 2009 07:21:43 +0000 (07:21 +0000)
More work in progress. Print column headers, and improve sorting. These classes are still not used.

lang/en_utf8/moodle.php
lang/en_utf8/question.php
mod/quiz/edit.php
mod/quiz/editlib.php
question/edit.php
question/editlib.php

index bd2928e238321f9f5016be16cb975486d6f27f57..bdfdafec172ec7cc4ea2e8c0f53026bce0bb9674 100644 (file)
@@ -1450,6 +1450,8 @@ $string['socialheadline'] = 'Social forum - latest topics';
 $string['someallowguest'] = 'Some courses may allow guest access';
 $string['someerrorswerefound'] = 'Some information was missing or incorrect. Look below for details.';
 $string['sortby'] = 'Sort by';
+$string['sortbyx'] = 'Sort by $a ascending';
+$string['sortbyxreverse'] = 'Sort by $a descending';
 $string['sourcerole'] = 'Source role';
 $string['specifyname'] = 'You must specify a name.';
 $string['standard'] = 'Standard';
index fb5c859d19af323364b0407029d8e0ef019e8365..1020a2c60367264647e0db613ad8a81429f84be2 100644 (file)
@@ -63,6 +63,7 @@ $string['cwrqpfsnoprob'] = 'No question categories in your site are affected by
 $string['copy']= 'Copy from $a and change links.';
 $string['contexterror'] = 'You shouldn\'t have got here if you\'re not moving a category to another context.';
 $string['created'] = 'Created';
+$string['createdby'] = 'Created by';
 $string['createdmodifiedheader'] = 'Created / Last Saved';
 $string['defaultfor'] = 'Default for $a';
 $string['defaultinfofor'] = 'The default category for questions shared in context \'$a\'.';
@@ -108,6 +109,7 @@ $string['invalidconfirm'] = 'Confirmation string was incorrect';
 $string['invalidcategoryidforparent'] = 'Invalid category id for parent!';
 $string['invalidcategoryidtomove'] = 'Invalid category id to move!';
 $string['invalidwizardpage'] = 'Incorrect or no wizard page specified!';
+$string['lastmodifiedby'] = 'Last modified by';
 $string['linkedfiledoesntexist'] = 'Linked file $a doesn\'t exist';
 $string['makechildof'] = "Make Child of '\$a'";
 $string['maketoplevelitem'] = 'Move to top level';
@@ -144,6 +146,7 @@ $string['permissionmove'] = 'Move this question';
 $string['permissionsaveasnew'] = 'Save this as a new question';
 $string['permissionto'] = 'You have permission to :';
 $string['published'] = 'shared';
+$string['qtypeveryshort'] = 'T';
 $string['questionaffected'] = '<a href=\"$a->qurl\">Question \"$a->name\" ($a->qtype)</a> is in this question category but is also being used in <a href=\"$a->qurl\">quiz \"$a->quizname\"</a> in another course \"$a->coursename\".';
 $string['questionbank'] = 'Question bank';
 $string['questioncategory'] = 'Question category';
index 5421b827c212e3e5731a943f2abd829d65a49f1e..18920133e9d19363c83e8bbbcf6a319bff1f981c 100644 (file)
@@ -154,7 +154,7 @@ if ($quiz_qbanktool > -1) {
 } else {
     $quiz_qbanktool = get_user_preferences("quiz_qbanktool_open",0);
 }
-$questionbank = new quiz_question_bank_view();
+$questionbank = new quiz_question_bank_view($contexts, $thispageurl, $cm);
 
 //will be set further down in the code
 $quiz_has_attempts=false;
@@ -631,7 +631,7 @@ echo '<span id="questionbank"></span>';
 echo '<div class="container">';
 echo '<div id="module" class="module">';
 echo '<div class="bd">';
-$questionbank->display('editq', $contexts, $thispageurl, $cm,
+$questionbank->display('editq',
         $pagevars['qpage'],
         $pagevars['qperpage'], $pagevars['qsortorder'],
         $pagevars['qsortorderdecoded'],
index c708dbd33c04647dcc71b46a3a53af4d809dc7d1..3e5c93158a7cb3555aabf8bb967e2dab1d14b065 100644 (file)
@@ -1014,8 +1014,8 @@ function quiz_question_tostring(&$question,$showicon=false,$showquestiontext=tru
 class quiz_question_bank_view extends question_bank_view {
     protected $quizhasattempts = false;
 
-    public function __construct() {
-
+    public function __construct($contexts, $pageurl, $cm = null) {
+        parent::__construct($contexts, $pageurl, $cm);
     }
 
     /**
@@ -1040,42 +1040,11 @@ class quiz_question_bank_view extends question_bank_view {
      * category      Chooses the category
      * displayoptions Sets display options
      */
-    function display($tabname, $contexts, $pageurl, $cm, $page, $perpage, $sortorder,
+    public function display($tabname, $page, $perpage, $sortorder,
             $sortorderdecoded, $cat, $recurse, $showhidden, $showquestiontext){
         global $COURSE, $DB;
 
-        if (optional_param('deleteselected', false, PARAM_BOOL)){ // teacher still has to confirm
-            // 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 ($DB->record_exists('quiz_question_instances', array('question' => $key))) {
-                        $questionnames .= '* ';
-                        $inuse = true;
-                    }
-                    $questionnames .= $DB->get_field('question', 'name', array('id' => $key)) . '<br />';
-                }
-            }
-            if (!$questionlist) { // no questions were selected
-                redirect($pageurl->out());
-            }
-            $questionlist = rtrim($questionlist, ',');
-
-            // Add an explanation about questions in use
-            if ($inuse) {
-                $questionnames .= '<br />'.get_string('questionsinuse', 'quiz');
-            }
-            notice_yesno(get_string("deletequestionscheck", "quiz", $questionnames),
-                        $pageurl->out_action(array('deleteselected'=>$questionlist, 'confirm'=>md5($questionlist))),
-                        $pageurl->out_action());
-
+        if ($this->process_actions_needing_ui()) {
             return;
         }
 
@@ -1112,17 +1081,17 @@ class quiz_question_bank_view extends question_bank_view {
 
         print_box_start('generalbox questionbank');
 
-        $this->display_category_form($contexts->having_one_edit_tab_cap($tabname),
-                $pageurl, $cat, $recurse, $showhidden, $showquestiontext);
+        $this->display_category_form($this->contexts->having_one_edit_tab_cap($tabname),
+                $this->baseurl, $cat, $recurse, $showhidden, $showquestiontext);
 
         // continues with list of questions
-        $this->display_question_list($contexts->having_one_edit_tab_cap($tabname), $pageurl, $cat, isset($cm) ? $cm : null,
+        $this->display_question_list($this->contexts->having_one_edit_tab_cap($tabname), $this->baseurl, $cat, $this->cm,
                 $recurse, $page, $perpage, $showhidden, $sortorder, $sortorderdecoded, $showquestiontext,
-                $contexts->having_cap('moodle/question:add'));
+                $this->contexts->having_cap('moodle/question:add'));
 
         echo '<hr/><form method="get" action="edit.php" id="displayoptions">';
         echo "<fieldset class='invisiblefieldset'>";
-        echo $pageurl->hidden_params_out(array('recurse', 'showhidden',
+        echo $this->baseurl->hidden_params_out(array('recurse', 'showhidden',
                 'showquestiontext'));
         $this->display_category_form_checkbox('recurse', $recurse);
         $this->display_category_form_checkbox('showhidden', $showhidden);
@@ -1136,7 +1105,7 @@ class quiz_question_bank_view extends question_bank_view {
     /**
      * prints a form to choose categories
      */
-    function display_category_form($contexts, $pageurl, $current, $recurse=1,
+    protected function display_category_form($contexts, $pageurl, $current, $recurse=1,
             $showhidden=false, $showquestiontext=false) {
         global $CFG;
 
@@ -1165,11 +1134,11 @@ class quiz_question_bank_view extends question_bank_view {
     * @param boolean $showhidden   True if also hidden questions should be displayed
     * @param boolean $showquestiontext whether the text of each question should be shown in the list
     */
-    function display_question_list($contexts, $pageurl, $categoryandcontext,
+    protected function display_question_list($contexts, $pageurl, $categoryandcontext,
             $cm = null, $recurse=1, $page=0, $perpage=100, $showhidden=false,
             $sortorder='typename', $sortorderdecoded='qtype, name ASC',
             $showquestiontext = false, $addcontexts = array()) {
-        global $USER, $CFG, $THEME, $COURSE, $DB;
+        global $CFG, $COURSE, $DB;
 
         list($categoryid, $contextid)=  explode(',', $categoryandcontext);
 
@@ -1274,7 +1243,7 @@ class quiz_question_bank_view extends question_bank_view {
 
             // There are no questions on the requested page.
             $page = 0;
-            if (!$questionsatall = $DB->get_records_select('question',
+            if (!$questions = $DB->get_records_select('question',
                     "category $usql AND parent = '0' $showhidden", $params, $sortorderdecoded,
                     '*', 0, $perpage)) {
                 // There are no questions at all
@@ -1426,23 +1395,157 @@ class quiz_question_bank_view extends question_bank_view {
         echo '</fieldset>';
         echo "</form>\n";
     }
-}
 
-/**
- * Add an arbitrary element to array at a specified index, pushing the rest
- * back.
- *
- * @param array $array The array to operate on
- * @param mixed $value The element to add
- * @param integer $at The position at which to add the element
- * @return array
- */
-function array_add_at($array,$value,$at){
-    $beginpart=array_slice($array, 0,$at);
-    $endpart=array_slice($array, $at, (count($array)-$at) );
-    $beginpart[]=$value;
-    $result=array_merge($beginpart,$endpart);
-    return $result;
+    protected function display_question_sort_options($pageurl, $sortorder){
+        //sort options
+        $html = "<div class=\"mdl-align questionsortoptions\">";
+        // POST method should only be used for parameters that change data
+        // or if POST method has to be used, the user must be redirected immediately to
+        // non-POSTed page to not break the back button
+        $html .= '<form method="get" action="edit.php">';
+        $html .= '<fieldset class="invisiblefieldset" style="display: block;">';
+        $html .= '<input type="hidden" name="sesskey" value="'.sesskey().'" />';
+        $html .= $pageurl->hidden_params_out(array('qsortorder'));
+        //choose_from_menu concatenates the form name with
+        //"menu" so the label is for menuqsortorder
+        $sortoptions = array('alpha' => get_string("qname", "quiz"),
+                             'typealpha' => get_string("qtypename", "quiz"),
+                             'age' => get_string("age", "quiz"));
+        $a =  choose_from_menu($sortoptions, 'qsortorder', $sortorder, false, 'this.form.submit();', '0', true);
+        $html .= '<label for="menuqsortorder">'.get_string('sortquestionsbyx', 'quiz', $a).'</label>';
+        $html .=  '<noscript><div><input type="submit" value="'.get_string("sortsubmit", "quiz").'" /></div></noscript>';
+        $html .= '</fieldset>';
+        $html .= "</form>\n";
+        $html .= "</div>\n";
+        echo $html;
+    }
+
+    public function process_actions() {
+        global $CFG, $COURSE, $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 = get_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 = "SELECT q.*, c.contextid FROM {question} q, {question_categories} c WHERE q.id $usql AND c.id = q.category";
+                if (!$questions = $DB->get_records_sql($sql, $params)){
+                    print_error('questiondoesnotexist', 'question', $pageurl->out());
+                }
+                $checkforfiles = false;
+                foreach ($questions as $question){
+                    //check capabilities
+                    question_require_capability_on($question, 'move');
+                    $fromcontext = get_context_instance_by_id($question->contextid);
+                    if (get_filesdir_from_context($fromcontext) != get_filesdir_from_context($tocontext)){
+                        $checkforfiles = true;
+                    }
+                }
+                $returnurl = $pageurl->out(false, array('category'=>"$tocategoryid,$contextid"));
+                if (!$checkforfiles){
+                    if (!question_move_questions_to_category(implode(',', $questionids), $tocategory->id)) {
+                        print_error('errormovingquestions', 'question', $returnurl, $questionids);
+                    }
+                    redirect($returnurl);
+                } else {
+                    $movecontexturl  = new moodle_url($CFG->wwwroot.'/question/contextmoveq.php',
+                                                    array('returnurl' => $returnurl,
+                                                            'ids'=>$questionidlist,
+                                                            'tocatid'=> $tocategoryid));
+                    if ($cm){
+                        $movecontexturl->param('cmid', $cm->id);
+                    } else {
+                        $movecontexturl->param('courseid', $COURSE->id);
+                    }
+                    redirect($movecontexturl->out());
+                }
+            }
+        }
+
+        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');
+                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) {
+                            question_require_capability_on($questionid, 'edit');
+                            if ($DB->record_exists('quiz_question_instances', array('question' => $questionid))) {
+                                if (!$DB->set_field('question', 'hidden', 1, array('id' => $questionid))) {
+                                    question_require_capability_on($questionid, 'edit');
+                                    print_error('cannothidequestion', 'question');
+                                }
+                            } else {
+                                delete_question($questionid);
+                            }
+                        }
+                    }
+                    redirect($pageurl->out());
+                } else {
+                    print_error('invalidconfirm', 'question');
+                }
+            }
+        }
+
+        // Unhide a question
+        if(($unhide = optional_param('unhide', '', PARAM_INT)) and confirm_sesskey()) {
+            question_require_capability_on($unhide, 'edit');
+            if(!$DB->set_field('question', 'hidden', 0, array('id', $unhide))) {
+                print_error('cannotunhidequestion', 'question');
+            }
+            redirect($pageurl->out());
+        }
+    }
+
+    public function process_actions_needing_ui() {
+        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 ($DB->record_exists('quiz_question_instances', array('question' => $key))) {
+                        $questionnames .= '* ';
+                        $inuse = true;
+                    }
+                    $questionnames .= $DB->get_field('question', 'name', array('id' => $key)) . '<br />';
+                }
+            }
+            if (!$questionlist) { // no questions were selected
+                redirect($this->baseurl->out());
+            }
+            $questionlist = rtrim($questionlist, ',');
+
+            // Add an explanation about questions in use
+            if ($inuse) {
+                $questionnames .= '<br />'.get_string('questionsinuse', 'quiz');
+            }
+            notice_yesno(get_string("deletequestionscheck", "quiz", $questionnames),
+                        $pageurl->out_action(array('deleteselected'=>$questionlist, 'confirm'=>md5($questionlist))),
+                        $pageurl->out_action());
+
+            return true;
+        }
+    }
 }
 
 /**
index 53804e0916ed56dc3a162f719063266807ac102d..de3bf6e9f52f310ca9b31a89d15c82e297bb2e22 100644 (file)
@@ -34,8 +34,8 @@
     require_once("editlib.php");
 
     list($thispageurl, $contexts, $cmid, $cm, $module, $pagevars) = question_edit_setup('questions');
-    $questionbank = new question_bank_view();
-    $questionbank->process_actions($thispageurl, $cm);
+    $questionbank = new question_bank_view($contexts, $thispageurl, $cm);
+    $questionbank->process_actions();
 
     // TODO log this page view.
 
@@ -72,7 +72,7 @@
     echo '<table class="boxaligncenter" border="0" cellpadding="2" cellspacing="0">';
     echo '<tr><td valign="top">';
 
-    $questionbank->display('questions', $contexts, $thispageurl, $cm, $pagevars['qpage'],
+    $questionbank->display('questions', $pagevars['qpage'],
             $pagevars['qperpage'], $pagevars['qsortorder'], $pagevars['qsortorderdecoded'],
             $pagevars['cat'], $pagevars['recurse'], $pagevars['showhidden'], $pagevars['showquestiontext']);
 
index 7c02d41217d28c6f7bfe14bcadbe8727d857b263..dace59203614c2d30fc978f2da4ae32ef5f7658a 100644 (file)
@@ -128,13 +128,97 @@ function question_can_delete_cat($todelete) {
 }
 
 abstract class question_bank_column_base {
+    protected $qbank;
+
+    /**
+     * 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() {
+    }
 
     /**
      * Output the column header cell.
      * @param integer $currentsort 0 for none. 1 for normal sort, -1 for reverse sort.
      */
-    public function display_header($currentsort) {
-        // TODO.
+    public function display_header() {
+        echo '<th class=header ' . $this->get_name() . '" scope="col">';
+        $sortable = $this->is_sortable();
+        $name = $this->get_name();
+        $title = $this->get_title();
+        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 implode(' / ', $links);
+        } else if ($sortable) {
+            echo $this->make_sort_link($name, $this->qbank->get_sort($name), $title);
+        } else {
+            echo $title;
+        }
+        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 function get_title() {
+        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($name, $currentsort, $title, $defaultreverse = false) {
+        $newsortreverse = $defaultreverse;
+        if ($currentsort) {
+            $newsortreverse = $currentsort > 0;
+        }
+        if ($newsortreverse) {
+            $tip = get_string('sortbyxreverse', '', $title);
+        } else {
+            $tip = get_string('sortbyx', '', $title);
+        }
+        echo '<a href="' . $this->qbank->new_sort_url($name, $newsortreverse) . '" title="' . $tip . '">';
+        echo $title;
+        if ($currentsort) {
+            echo $this->get_sort_icon($currentsort < 0);
+        }
+        echo '</a>';
+    }
+
+    /**
+     * 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 $CFG;
+        if ($reverse) {
+            return ' <img src="' . $CFG->pixpath . '/t/up.gif" alt="' . get_string('desc') . '" />';
+        } else {
+            return ' <img src="' . $CFG->pixpath . '/t/down.gif" alt="' . get_string('asc') . '" />';
+        }
     }
 
     /**
@@ -149,16 +233,14 @@ abstract class question_bank_column_base {
     }
 
     protected function display_start($question, $rowclasses) {
-        echo '<td class="' . $this->get_css_classes($question, $rowclasses) . '">';
+        echo '<td class="' . $this->get_name() . '">';
     }
 
     /**
      * @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.
-     * @return string CSS class names that should be applied to this column. Should normally be
-     *      a simple work relating the the column type like 'questionname'.
+     * @return string internal name for this column. Used as a CSS class name, and to store information about the current sort.
      */
-    abstract protected function get_css_classes($question, $rowclasses);
+    abstract protected function get_name();
 
     /**
      * Output the contents of this column.
@@ -168,7 +250,7 @@ abstract class question_bank_column_base {
     abstract protected function display_content($question, $rowclasses);
 
     protected function display_end($question, $rowclasses) {
-        echo '</td>';
+        echo "</td>\n";
     }
 
     /**
@@ -197,10 +279,15 @@ abstract class question_bank_column_base {
     /**
      * Can this column be sorted on? You can return either:
      *  + false for no (the default),
-     *  + true for yes, or
-     *  + an array of subnames to sort on. E.g. 'firstname', 'lastname'.
-     * If this method returns true, sort_expression must be implemented.
-     * @return mixed  
+     *  + 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;
@@ -212,22 +299,11 @@ abstract class question_bank_column_base {
      * @param string $normaldir 'ASC' or 'DESC'
      * @return string 'ASC' or 'DESC'
      */
-    protected function sortorder($reverse, $normaldir = 'ASC') {
-        switch ($normaldir) {
-            case 'ASC':
-                if ($reverse) {
-                    return ' DESC';
-                } else {
-                    return ' ASC';
-                }
-            case 'DESC':
-                if ($reverse) {
-                    return ' ASC';
-                } else {
-                    return ' DESC';
-                }
-            default:
-                throw new coding_exception('$normaldir should be ASC or DESC.');
+    protected function sortorder($reverse) {
+        if ($reverse) {
+            return ' DESC';
+        } else {
+            return ' ASC';
         }
     }
 
@@ -238,7 +314,18 @@ abstract class question_bank_column_base {
      * @return string some SQL to go in the order by clause.
      */
     public function sort_expression($reverse, $subsort) {
-        throw new coding_exception('A subclass of question_bank_column_base must implement sort_expression if is_sortable returns true.');
+        $sortable = $this->is_sortable();
+        if (is_array($sortable)) {
+            if (array_key_exists($subsort, $sortable)) {
+                return $sortable[$sortable]['field'] . $this->sortorder($reverse, !empty($sortable[$sortable]['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.');
+        }
     }
 }
 
@@ -248,15 +335,18 @@ abstract class question_bank_column_base {
 class question_bank_checkbox_column extends question_bank_column_base {
     protected $strselect;
 
-    public function __construct() {
-        parent::__construct();
+    public function init() {
         $this->strselect = get_string('select', 'quiz');
     }
 
-    protected function get_css_classes($question, $rowclasses) {
+    protected function get_name() {
         return 'checkbox';
     }
 
+    protected function get_title() {
+        return '<input type="checkbox" disabled="disabled" />';
+    }
+
     protected function display_content($question, $rowclasses) {
         echo '<input title="' . $this->strselect . '" type="checkbox" name="q' .
                 $question->id . '" id="checkq' . $question->id . '" value="1" />';
@@ -271,10 +361,14 @@ class question_bank_checkbox_column extends question_bank_column_base {
  * A column type for the name of the question type.
  */
 class question_bank_question_type_column extends question_bank_column_base {
-    protected function get_css_classes($question, $rowclasses) {
+    protected function get_name() {
         return 'qtype';
     }
 
+    protected function get_title() {
+        return get_string('qtypeveryshort', 'question');
+    }
+
     protected function display_content($question, $rowclasses) {
         echo print_question_icon($question);
     }
@@ -284,11 +378,7 @@ class question_bank_question_type_column extends question_bank_column_base {
     }
 
     public function is_sortable() {
-        return true;
-    }
-
-    public function sort_expression($reverse, $subsort) {
-        return 'q.qtype' . $this->sortorder($reverse);
+        return 'q.qtype';
     }
 }
 
@@ -296,10 +386,14 @@ class question_bank_question_type_column extends question_bank_column_base {
  * A column type for the name of the question name.
  */
 class question_bank_question_name_column extends question_bank_column_base {
-    protected function get_css_classes($question, $rowclasses) {
+    protected function get_name() {
         return 'questionname';
     }
 
+    protected function get_title() {
+        return get_string('question');
+    }
+
     protected function display_content($question, $rowclasses) {
         echo format_string($question->name);
     }
@@ -309,11 +403,7 @@ class question_bank_question_name_column extends question_bank_column_base {
     }
 
     public function is_sortable() {
-        return true;
-    }
-
-    public function sort_expression($reverse, $subsort) {
-        return 'q.name' . $this->sortorder($reverse);
+        return 'q.name';
     }
 }
 
@@ -321,10 +411,14 @@ class question_bank_question_name_column extends question_bank_column_base {
  * A column type for the name of the question creator.
  */
 class question_bank_creator_name_column extends question_bank_column_base {
-    protected function get_css_classes($question, $rowclasses) {
+    protected 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;
@@ -343,15 +437,10 @@ class question_bank_creator_name_column extends question_bank_column_base {
     }
 
     public function is_sortable() {
-        return array('firstname', 'lastname');
-    }
-
-    public function sort_expression($reverse, $subsort) {
-        if (in_array($subsort, $this->is_sortable())) {
-            return 'uc.' . $subsort . $this->sortorder($reverse);
-        } else {
-            throw new coding_exception('Unexpected $subsort type.');
-        }
+        return array(
+            'firstname' => array('field' => 'uc.firstname', 'title' => get_string('firstname')),
+            'lastname' => array('field' => 'uc.lastname', 'title' => get_string('lastname')),
+        );
     }
 }
 
@@ -359,10 +448,14 @@ class question_bank_creator_name_column extends question_bank_column_base {
  * A column type for the name of the question last modifier.
  */
 class question_bank_modifier_name_column extends question_bank_column_base {
-    protected function get_css_classes($question, $rowclasses) {
+    protected 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;
@@ -381,15 +474,10 @@ class question_bank_modifier_name_column extends question_bank_column_base {
     }
 
     public function is_sortable() {
-        return array('firstname', 'lastname');
-    }
-
-    public function sort_expression($reverse, $subsort) {
-        if (in_array($subsort, $this->is_sortable())) {
-            return 'um.' . $subsort . $this->sortorder($reverse);
-        } else {
-            throw new coding_exception('Unexpected $subsort type.');
-        }
+        return array(
+            'firstname' => array('field' => 'um.firstname', 'title' => get_string('firstname')),
+            'lastname' => array('field' => 'um.lastname', 'title' => get_string('lastname')),
+        );
     }
 }
 
@@ -397,6 +485,11 @@ class question_bank_modifier_name_column extends question_bank_column_base {
  * A base class for actions that are an icon that lets you manipulate the question in some way.
  */
 abstract class question_bank_action_column_base extends question_bank_column_base {
+
+    protected function get_title() {
+        return '&#160;';
+    }
+
     protected function print_icon($icon, $title, $url) {
         global $CFG;
         echo '<a title="' . $title . '" href="' . $url . '">
@@ -411,47 +504,42 @@ abstract class question_bank_action_column_base extends question_bank_column_bas
 class question_bank_edit_action_column extends question_bank_action_column_base {
     protected $stredit;
     protected $strview;
-    protected $questionurl;
 
-    public function __construct($questionurl) {
-        parent::__construct();
-        $this->questionurl = $questionurl;
+    public function init() {
+        parent::init();
         $this->stredit = get_string('edit');
         $this->strview = get_string('view');
     }
 
-    protected function get_css_classes($question, $rowclasses) {
+    protected function get_name() {
         return 'editaction';
     }
 
     protected function display_content($question, $rowclasses) {
         if (question_has_capability_on($question, 'edit') ||
                 question_has_capability_on($question, 'move')) {
-            $this->print_icon('edit', $this->stredit, $this->questionurl->out(false, array('id' => $question->id)));
+            $this->print_icon('edit', $this->stredit, $this->qbank->edit_question_url($question->id));
         } else {
-            $this->print_icon('info', $this->strview, $this->questionurl->out(false, array('id' => $question->id)));
+            $this->print_icon('info', $this->strview, $this->qbank->edit_question_url($question->id));
         }
     }
 }
 
 class question_bank_preview_action_column extends question_bank_action_column_base {
     protected $strpreview;
-    protected $quizorcourseid;
 
-    public function __construct($quizorcourseid) {
-        parent::__construct();
-        $this->quizorcourseid = $quizorcourseid;
+    public function init() {
+        parent::init();
         $this->stredit = get_string('preview');
     }
 
-    protected function get_css_classes($question, $rowclasses) {
+    protected function get_name() {
         return 'previewaction';
     }
 
     protected function display_content($question, $rowclasses) {
         if (question_has_capability_on($question, 'use')) {
-            link_to_popup_window('/question/preview.php?id=' . $question->id .
-                    $this->quizorcourseid, 'questionpreview',
+            link_to_popup_window($this->qbank->preview_question_url($question->id), 'questionpreview',
                     ' <img src="' . $CFG->pixpath . '/t/preview.gif" class="iconsmall" alt="' . $this->strpreview . '" />',
                     0, 0, $this->strpreview, QUESTION_PREVIEW_POPUP_OPTIONS);
         }
@@ -464,21 +552,19 @@ class question_bank_preview_action_column extends question_bank_action_column_ba
 
 class question_bank_move_action_column extends question_bank_action_column_base {
     protected $strmove;
-    protected $questionurl;
 
-    public function __construct($questionurl) {
-        parent::__construct();
-        $this->questionurl = $questionurl;
+    public function init() {
+        parent::init();
         $this->strmove = get_string('move');
     }
 
-    protected function get_css_classes($question, $rowclasses) {
+    protected function get_name() {
         return 'editaction';
     }
 
     protected function display_content($question, $rowclasses) {
         if (question_has_capability_on($question, 'move')) {
-            $this->print_icon('move', $this->strmove, $this->questionurl->out(false, array('id' => $question->id)));
+            $this->print_icon('move', $this->strmove, $this->qbank->edit_question_url($question->id));
         }
     }
 }
@@ -489,26 +575,24 @@ class question_bank_move_action_column extends question_bank_action_column_base
 class question_bank_delete_action_column extends question_bank_action_column_base {
     protected $strdelete;
     protected $strrestore;
-    protected $questionurl;
 
-    public function __construct($questionurl) {
-        parent::__construct();
-        $this->questionurl = $questionurl;
+    public function init() {
+        parent::init();
         $this->strdelete = get_string('delete');
         $this->strrestore = get_string('restore');
     }
 
-    protected function get_css_classes($question, $rowclasses) {
+    protected function get_name() {
         return 'deleteaction';
     }
 
     protected function display_content($question, $rowclasses) {
         if (question_has_capability_on($question, 'edit')) {
             if ($question->hidden) {
-                $this->print_icon('restore', $this->strrestore, $pageurl->out(false, array('unhide' => $question->id)));
+                $this->print_icon('restore', $this->strrestore, $this->qbank->base_url()->out(false, array('unhide' => $question->id)));
             } else {
                 $this->print_icon('restore', $this->strrestore,
-                        $pageurl->out(false, array('deleteselected' => $question->id, 'q' . $question->id => 1)));
+                        $this->qbank->base_url()->out(false, array('deleteselected' => $question->id, 'q' . $question->id => 1)));
             }
         }
     }
@@ -536,8 +620,46 @@ class question_bank_delete_action_column extends question_bank_action_column_bas
  *  + outputting table headers.
  */
 class question_bank_view {
-    public function __construct() {
+    protected $baseurl;
+    protected $editquestionurl;
+    protected $quizorcourseid;
+    protected $contexts;
+    protected $cm;
+
+    public function __construct($contexts, $pageurl, $cm = null) {
+        global $CFG, $COURSE;
 
+        $this->contexts = $contexts;
+        $this->baseurl = $pageurl;
+        $this->cm = $cm;
+
+        if (!empty($cm) && $cm->modname == 'quiz') {
+            $this->quizorcourseid = '&amp;quizid=' . $cm->instance;
+        } else {
+            $this->quizorcourseid = '&amp;courseid=' .$COURSE->id;
+        }
+
+        // Create the url of the new question page to forward to.
+        $this->editquestionurl = new moodle_url("$CFG->wwwroot/question/question.php",
+                array('returnurl' => $pageurl->out()));
+        if ($cm !== null){
+            $this->editquestionurl->param('cmid', $cm->id);
+        } else {
+            $this->editquestionurl->param('courseid', $COURSE->id);
+        }
+    }
+
+    public function base_url($questionid) {
+        return $baseurl;
+    }
+
+    public function edit_question_url($questionid) {
+        return $this->editquestionurl->out(false, array('id' => $questionid));
+    }
+
+    public function preview_question_url($questionid) {
+        global $CFG;
+        return $CFG->wwwroot . '/question/preview.php?id=' . $question->id . $this->quizorcourseid;
     }
 
     /**
@@ -552,42 +674,11 @@ class question_bank_view {
      * category      Chooses the category
      * displayoptions Sets display options
      */
-    function display($tabname, $contexts, $pageurl, $cm, $page, $perpage, $sortorder,
+    public function display($tabname, $page, $perpage, $sortorder,
             $sortorderdecoded, $cat, $recurse, $showhidden, $showquestiontext){
         global $COURSE, $DB;
 
-        if (optional_param('deleteselected', false, PARAM_BOOL)){ // teacher still has to confirm
-            // 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 ($DB->record_exists('quiz_question_instances', array('question' => $key))) {
-                        $questionnames .= '* ';
-                        $inuse = true;
-                    }
-                    $questionnames .= $DB->get_field('question', 'name', array('id' => $key)) . '<br />';
-                }
-            }
-            if (!$questionlist) { // no questions were selected
-                redirect($pageurl->out());
-            }
-            $questionlist = rtrim($questionlist, ',');
-
-            // Add an explanation about questions in use
-            if ($inuse) {
-                $questionnames .= '<br />'.get_string('questionsinuse', 'quiz');
-            }
-            notice_yesno(get_string("deletequestionscheck", "quiz", $questionnames),
-                        $pageurl->out_action(array('deleteselected'=>$questionlist, 'confirm'=>md5($questionlist))),
-                        $pageurl->out_action());
-
+        if ($this->process_actions_needing_ui()) {
             return;
         }
 
@@ -595,13 +686,13 @@ class question_bank_view {
         print_box_start('generalbox questionbank');
         print_heading(get_string('questionbank', 'question'), '', 2);
 
-        $this->display_category_form($contexts->having_one_edit_tab_cap($tabname),
-                $pageurl, $cat, $recurse, $showhidden, $showquestiontext);
+        $this->display_category_form($this->contexts->having_one_edit_tab_cap($tabname),
+                $this->baseurl, $cat, $recurse, $showhidden, $showquestiontext);
 
         // continues with list of questions
-        $this->display_question_list($contexts->having_one_edit_tab_cap($tabname), $pageurl, $cat, isset($cm) ? $cm : null,
+        $this->display_question_list($this->contexts->having_one_edit_tab_cap($tabname), $this->baseurl, $cat, $this->cm,
                 $recurse, $page, $perpage, $showhidden, $sortorder, $sortorderdecoded, $showquestiontext,
-                $contexts->having_cap('moodle/question:add'));
+                $this->contexts->having_cap('moodle/question:add'));
 
         print_box_end();
     }
@@ -609,7 +700,7 @@ class question_bank_view {
     /**
      * prints a form to choose categories
      */
-    function display_category_form($contexts, $pageurl, $current, $recurse=1,
+    protected function display_category_form($contexts, $pageurl, $current, $recurse=1,
             $showhidden=false, $showquestiontext=false) {
         global $CFG;
 
@@ -661,11 +752,11 @@ class question_bank_view {
     * @param boolean $showhidden   True if also hidden questions should be displayed
     * @param boolean $showquestiontext whether the text of each question should be shown in the list
     */
-    function display_question_list($contexts, $pageurl, $categoryandcontext,
+    protected function display_question_list($contexts, $pageurl, $categoryandcontext,
             $cm = null, $recurse=1, $page=0, $perpage=100, $showhidden=false,
             $sortorder='typename', $sortorderdecoded='qtype, name ASC',
             $showquestiontext = false, $addcontexts = array()) {
-        global $USER, $CFG, $THEME, $COURSE, $DB;
+        global $CFG, $COURSE, $DB;
 
         list($categoryid, $contextid)=  explode(',', $categoryandcontext);
 
@@ -945,8 +1036,7 @@ class question_bank_view {
         echo "</form>\n";
     }
 
-    function display_question_sort_options($pageurl, $sortorder){
-        global $USER;
+    protected function display_question_sort_options($pageurl, $sortorder){
         //sort options
         $html = "<div class=\"mdl-align questionsortoptions\">";
         // POST method should only be used for parameters that change data
@@ -970,7 +1060,7 @@ class question_bank_view {
         echo $html;
     }
 
-    function process_actions($pageurl, $cm){
+    public function process_actions() {
         global $CFG, $COURSE, $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
@@ -1059,6 +1149,43 @@ class question_bank_view {
             redirect($pageurl->out());
         }
     }
+
+    public function process_actions_needing_ui() {
+        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 ($DB->record_exists('quiz_question_instances', array('question' => $key))) {
+                        $questionnames .= '* ';
+                        $inuse = true;
+                    }
+                    $questionnames .= $DB->get_field('question', 'name', array('id' => $key)) . '<br />';
+                }
+            }
+            if (!$questionlist) { // no questions were selected
+                redirect($this->baseurl->out());
+            }
+            $questionlist = rtrim($questionlist, ',');
+
+            // Add an explanation about questions in use
+            if ($inuse) {
+                $questionnames .= '<br />'.get_string('questionsinuse', 'quiz');
+            }
+            notice_yesno(get_string("deletequestionscheck", "quiz", $questionnames),
+                        $pageurl->out_action(array('deleteselected'=>$questionlist, 'confirm'=>md5($questionlist))),
+                        $pageurl->out_action());
+
+            return true;
+        }
+    }
 }
 
 /**