From 17f229fae3e847264d4422e5ffa28e806e61061c Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Tue, 11 Mar 2014 09:16:33 -0500 Subject: [PATCH] MDL-40457 Question Bank: Allow plugins to add columns to question bank view 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. --- mod/quiz/editlib.php | 35 +- question/classes/bank/action_column_base.php | 46 + question/classes/bank/checkbox_column.php | 60 + question/classes/bank/column_base.php | 312 ++++ question/classes/bank/copy_action_column.php | 48 + question/classes/bank/creator_name_column.php | 63 + .../classes/bank/delete_action_column.php | 56 + question/classes/bank/edit_action_column.php | 46 + .../classes/bank/modifier_name_column.php | 62 + .../classes/bank/preview_action_column.php | 41 + .../classes/bank/question_name_column.php | 65 + question/classes/bank/question_text_row.php | 62 + .../classes/bank/question_type_column.php | 49 + question/classes/bank/row_base.php | 42 + question/classes/bank/view.php | 923 ++++++++++ question/edit.php | 2 +- question/editlib.php | 1541 +---------------- question/upgrade.txt | 8 + 18 files changed, 1935 insertions(+), 1526 deletions(-) create mode 100644 question/classes/bank/action_column_base.php create mode 100644 question/classes/bank/checkbox_column.php create mode 100644 question/classes/bank/column_base.php create mode 100644 question/classes/bank/copy_action_column.php create mode 100644 question/classes/bank/creator_name_column.php create mode 100644 question/classes/bank/delete_action_column.php create mode 100644 question/classes/bank/edit_action_column.php create mode 100644 question/classes/bank/modifier_name_column.php create mode 100644 question/classes/bank/preview_action_column.php create mode 100644 question/classes/bank/question_name_column.php create mode 100644 question/classes/bank/question_text_row.php create mode 100644 question/classes/bank/question_type_column.php create mode 100644 question/classes/bank/row_base.php create mode 100644 question/classes/bank/view.php diff --git a/mod/quiz/editlib.php b/mod/quiz/editlib.php index 47a10867c11..e98e5948e77 100644 --- a/mod/quiz/editlib.php +++ b/mod/quiz/editlib.php @@ -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 index 00000000000..6d2cd2f9a92 --- /dev/null +++ b/question/classes/bank/action_column_base.php @@ -0,0 +1,46 @@ +. + +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 ' '; + } + + public function get_extra_classes() { + return array('iconcol'); + } + + protected function print_icon($icon, $title, $url) { + global $OUTPUT; + echo ' + ' . $title . ''; + } + + 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 index 00000000000..9ba0ac35e0d --- /dev/null +++ b/question/classes/bank/checkbox_column.php @@ -0,0 +1,60 @@ +. + +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 ''; + } + + protected function get_title_tip() { + return get_string('selectquestionsforbulk', 'question'); + } + + protected function display_content($question, $rowclasses) { + global $PAGE; + echo ''; + 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 index 00000000000..b300ec7df67 --- /dev/null +++ b/question/classes/bank/column_base.php @@ -0,0 +1,312 @@ +. + +/** + * + * @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 ''; + $sortable = $this->is_sortable(); + $name = get_class($this); + $title = $this->get_title(); + $tip = $this->get_title_tip(); + if (is_array($sortable)) { + if ($title) { + echo '
' . $title . '
'; + } + $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, $title, $tip); + } else { + if ($tip) { + echo ''; + } + echo $title; + if ($tip) { + echo ''; + } + } + echo "\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 = ''; + $link .= $title; + if ($currentsort) { + $link .= $this->get_sort_icon($currentsort < 0); + } + $link .= ''; + 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 tag instead of + * + * @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 index 00000000000..9d0d890e64c --- /dev/null +++ b/question/classes/bank/copy_action_column.php @@ -0,0 +1,48 @@ +. + +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 index 00000000000..6b13efa7c03 --- /dev/null +++ b/question/classes/bank/creator_name_column.php @@ -0,0 +1,63 @@ +. + + +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 index 00000000000..7ef065e7ae1 --- /dev/null +++ b/question/classes/bank/delete_action_column.php @@ -0,0 +1,56 @@ +. + +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 index 00000000000..404596919c0 --- /dev/null +++ b/question/classes/bank/edit_action_column.php @@ -0,0 +1,46 @@ +. + +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 index 00000000000..6981f34ca87 --- /dev/null +++ b/question/classes/bank/modifier_name_column.php @@ -0,0 +1,62 @@ +. + + +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 index 00000000000..f31d5cc359f --- /dev/null +++ b/question/classes/bank/preview_action_column.php @@ -0,0 +1,41 @@ +. + +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 index 00000000000..05d18c6df76 --- /dev/null +++ b/question/classes/bank/question_name_column.php @@ -0,0 +1,65 @@ +. + +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 ''; + } + } + + 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 index 00000000000..cf9df126bff --- /dev/null +++ b/question/classes/bank/question_text_row.php @@ -0,0 +1,62 @@ +. + + +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 = ' '; + } + 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 index 00000000000..da7c83928ba --- /dev/null +++ b/question/classes/bank/question_type_column.php @@ -0,0 +1,49 @@ +. + +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 index 00000000000..a091c95baa4 --- /dev/null +++ b/question/classes/bank/row_base.php @@ -0,0 +1,42 @@ +. + +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 '' . "\n"; + } else { + echo "\n"; + } + echo ''; + } + + protected function display_end($question, $rowclasses) { + echo "\n"; + } +} diff --git a/question/classes/bank/view.php b/question/classes/bank/view.php new file mode 100644 index 00000000000..022be821f13 --- /dev/null +++ b/question/classes/bank/view.php @@ -0,0 +1,923 @@ +. + + +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 = '&quizid=' . $cm->instance; + } else { + $this->quizorcourseid = '&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 "

"; + print_string('selectcategoryabove', 'question'); + echo "

"; + } + + 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 '
'; + echo format_text($category->info, $category->infoformat, $formatoptions, $this->course->id); + echo "
\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 '
'; + $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 "
\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 '
'; + echo ''; + echo ''; + echo "
\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 '
'; + echo "
"; + 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 '
'; + } + + /** + * 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 '
'; + 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 "
\n"; + } + + protected function create_new_question_form($category, $canadd) { + global $CFG; + echo '
'; + if ($canadd) { + create_new_question_button($category->id, $this->editquestionurl->params(), + get_string('createnewquestion', 'question')); + } else { + print_string('nopermissionadd', 'question'); + } + echo '
'; + } + + /** + * 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 '
'; + $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 '
'; + + echo '
'; + echo '
'; + echo ''; + echo \html_writer::input_hidden_params($this->baseurl); + + echo '
'; + $this->start_table(); + $rowcount = 0; + foreach ($questions as $question) { + $this->print_table_row($question, $rowcount); + $rowcount += 1; + } + $this->end_table(); + echo "
\n"; + + echo '
'; + 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 = ''.get_string('showall', 'moodle', $totalnumber).''; + } else { + $url = new \moodle_url('edit.php', array_merge($pageurl->params(), + array('qperpage' => DEFAULT_QUESTIONS_PER_PAGE))); + $showall = ''.get_string('showperpage', 'moodle', DEFAULT_QUESTIONS_PER_PAGE).''; + } + echo "
$showall
"; + } + echo '
'; + + echo '
'; + if ($caneditall || $canmoveall || $canuseall) { + echo ' '.get_string('withselected', 'question').':
'; + + if (function_exists('module_specific_buttons')) { + echo module_specific_buttons($this->cm->id, $cmoptions); + } + + // Print delete and move selected question. + if ($caneditall) { + echo '\n"; + } + + if ($canmoveall && count($addcontexts)) { + echo '\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 "
$modulespecific"; + } + } + } + echo "
\n"; + + echo '
'; + echo "
\n"; + } + + protected function start_table() { + echo '' . "\n"; + echo "\n"; + $this->print_table_headers(); + echo "\n"; + echo "\n"; + } + + protected function end_table() { + echo "\n"; + echo "
\n"; + } + + protected function print_table_headers() { + echo "\n"; + foreach ($this->visiblecolumns as $column) { + $column->display_header(); + } + echo "\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 '' . "\n"; + } else { + echo "\n"; + } + foreach ($this->visiblecolumns as $column) { + $column->display($question, $rowclasses); + } + echo "\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
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)) . '
'; + } + } + if (!$questionlist) { // No questions were selected. + redirect($this->baseurl); + } + $questionlist = rtrim($questionlist, ','); + + // Add an explanation about questions in use. + if ($inuse) { + $questionnames .= '
'.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; + } +} diff --git a/question/edit.php b/question/edit.php index 58fe57c2e52..b8cdb7fcd95 100644 --- a/question/edit.php +++ b/question/edit.php @@ -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. diff --git a/question/editlib.php b/question/editlib.php index 4048c1e2ead..27397000f27 100644 --- a/question/editlib.php +++ b/question/editlib.php @@ -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 ''; - $sortable = $this->is_sortable(); - $name = $this->get_name(); - $title = $this->get_title(); - $tip = $this->get_title_tip(); - if (is_array($sortable)) { - if ($title) { - echo '
' . $title . '
'; - } - $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, $title, $tip); - } else { - if ($tip) { - echo ''; - } - echo $title; - if ($tip) { - echo ''; - } - } - echo "\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 = ''; - $link .= $title; - if ($currentsort) { - $link .= $this->get_sort_icon($currentsort < 0); - } - $link .= ''; - 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 tag instead of - * - * @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 ''; - } - - protected function get_title_tip() { - return get_string('selectquestionsforbulk', 'question'); - } - - protected function display_content($question, $rowclasses) { - global $PAGE; - echo ''; - 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 ''; - } - } - - 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 ' '; - } - - public function get_extra_classes() { - return array('iconcol'); - } - - protected function print_icon($icon, $title, $url) { - global $OUTPUT; - echo ' - ' . $title . ''; - } - - 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 '' . "\n"; - } else { - echo "\n"; - } - echo ''; - } - - protected function display_end($question, $rowclasses) { - echo "\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 = ' '; - } - 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 = '&quizid=' . $cm->instance; - } else { - $this->quizorcourseid = '&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 "

"; - print_string('selectcategoryabove', 'question'); - echo "

"; - } - - 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 '
'; - echo format_text($category->info, $category->infoformat, $formatoptions, $this->course->id); - echo "
\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 '
'; - $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 "
\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 '
'; - echo ''; - echo ''; - echo "
\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 '
'; - echo "
"; - 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 '
'; - } - - /** - * 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 '
'; - 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 "
\n"; - } - - protected function create_new_question_form($category, $canadd) { - global $CFG; - echo '
'; - if ($canadd) { - create_new_question_button($category->id, $this->editquestionurl->params(), - get_string('createnewquestion', 'question')); - } else { - print_string('nopermissionadd', 'question'); - } - echo '
'; - } - - /** - * 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 '
'; - $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 '
'; - - echo '
'; - echo '
'; - echo ''; - echo html_writer::input_hidden_params($this->baseurl); - - echo '
'; - $this->start_table(); - $rowcount = 0; - foreach ($questions as $question) { - $this->print_table_row($question, $rowcount); - $rowcount += 1; - } - $this->end_table(); - echo "
\n"; - - echo '
'; - 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 = ''.get_string('showall', 'moodle', $totalnumber).''; - } else { - $url = new moodle_url('edit.php', array_merge($pageurl->params(), array('qperpage'=>DEFAULT_QUESTIONS_PER_PAGE))); - $showall = ''.get_string('showperpage', 'moodle', DEFAULT_QUESTIONS_PER_PAGE).''; - } - echo "
$showall
"; - } - echo '
'; - - echo '
'; - if ($caneditall || $canmoveall || $canuseall){ - echo ' '.get_string('withselected', 'question').':
'; - - if (function_exists('module_specific_buttons')) { - echo module_specific_buttons($this->cm->id,$cmoptions); - } - - // print delete and move selected question - if ($caneditall) { - echo '\n"; - } - - if ($canmoveall && count($addcontexts)) { - echo '\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 "
$modulespecific"; - } - } - } - echo "
\n"; - - echo '
'; - echo "
\n"; - } - - protected function start_table() { - echo '' . "\n"; - echo "\n"; - $this->print_table_headers(); - echo "\n"; - echo "\n"; - } - - protected function end_table() { - echo "\n"; - echo "
\n"; - } - - protected function print_table_headers() { - echo "\n"; - foreach ($this->visiblecolumns as $column) { - $column->display_header(); - } - echo "\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 '' . "\n"; - } else { - echo "\n"; - } - foreach ($this->visiblecolumns as $column) { - $column->display($question, $rowclasses); - } - echo "\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
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)) . '
'; - } - } - if (!$questionlist) { // no questions were selected - redirect($this->baseurl); - } - $questionlist = rtrim($questionlist, ','); - - // Add an explanation about questions in use - if ($inuse) { - $questionnames .= '
'.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); diff --git a/question/upgrade.txt b/question/upgrade.txt index f755b385ea9..4abcef00881 100644 --- a/question/upgrade.txt +++ b/question/upgrade.txt @@ -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 ===