. 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 '