2014-03-11 09:16:33 -05:00
|
|
|
<?php
|
|
|
|
// This file is part of Moodle - http://moodle.org/
|
|
|
|
//
|
|
|
|
// Moodle is free software: you can redistribute it and/or modify
|
|
|
|
// it under the terms of the GNU General Public License as published by
|
|
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
|
|
// (at your option) any later version.
|
|
|
|
//
|
|
|
|
// Moodle is distributed in the hope that it will be useful,
|
|
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
// GNU General Public License for more details.
|
|
|
|
//
|
|
|
|
// You should have received a copy of the GNU General Public License
|
|
|
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2019-10-02 13:51:48 +01:00
|
|
|
* Class to print a view of the question bank.
|
2014-03-11 09:16:33 -05:00
|
|
|
*
|
2019-10-02 13:51:48 +01:00
|
|
|
* @package core_question
|
|
|
|
* @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com}
|
|
|
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
2014-03-11 09:16:33 -05:00
|
|
|
*/
|
|
|
|
|
2019-10-02 13:51:48 +01:00
|
|
|
namespace core_question\bank;
|
|
|
|
defined('MOODLE_INTERNAL') || die();
|
|
|
|
|
|
|
|
use core_question\bank\search\condition;
|
|
|
|
|
2014-03-11 09:16:33 -05:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*
|
2019-10-02 13:51:48 +01:00
|
|
|
* @copyright 2009 Tim Hunt
|
|
|
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
2014-03-11 09:16:33 -05:00
|
|
|
*/
|
|
|
|
class view {
|
|
|
|
const MAX_SORTS = 3;
|
|
|
|
|
2019-09-27 13:57:29 +01:00
|
|
|
/**
|
|
|
|
* @var \moodle_url base URL for the current page. Used as the
|
|
|
|
* basis for making URLs for actions that reload the page.
|
|
|
|
*/
|
2014-03-11 09:16:33 -05:00
|
|
|
protected $baseurl;
|
2019-09-27 13:57:29 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @var \moodle_url used as a basis for URLs that edit a question.
|
|
|
|
*/
|
2014-03-11 09:16:33 -05:00
|
|
|
protected $editquestionurl;
|
2019-09-27 13:57:29 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @var \question_edit_contexts
|
|
|
|
*/
|
2014-03-11 09:16:33 -05:00
|
|
|
protected $contexts;
|
2019-09-27 13:57:29 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @var object|\cm_info|null if we are in a module context, the cm.
|
|
|
|
*/
|
2014-03-11 09:16:33 -05:00
|
|
|
protected $cm;
|
2019-09-27 13:57:29 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @var object the course we are within.
|
|
|
|
*/
|
2014-03-11 09:16:33 -05:00
|
|
|
protected $course;
|
2019-09-27 13:57:29 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @var \question_bank_column_base[] these are all the 'columns' that are
|
|
|
|
* part of the display. Array keys are the class name.
|
|
|
|
*/
|
|
|
|
protected $requiredcolumns;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var \question_bank_column_base[] these are the 'columns' that are
|
|
|
|
* actually displayed as a column, in order. Array keys are the class name.
|
|
|
|
*/
|
2014-03-11 09:16:33 -05:00
|
|
|
protected $visiblecolumns;
|
2019-09-27 13:57:29 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @var \question_bank_column_base[] these are the 'columns' that are
|
|
|
|
* actually displayed as an additional row (e.g. question text), in order.
|
|
|
|
* Array keys are the class name.
|
|
|
|
*/
|
2014-03-11 09:16:33 -05:00
|
|
|
protected $extrarows;
|
2019-09-27 13:57:29 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @var array list of column class names for which columns to sort on.
|
|
|
|
*/
|
2014-03-11 09:16:33 -05:00
|
|
|
protected $sort;
|
2019-09-27 13:57:29 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @var int|null id of the a question to highlight in the list (if present).
|
|
|
|
*/
|
2014-03-11 09:16:33 -05:00
|
|
|
protected $lastchangedid;
|
2019-09-27 13:57:29 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @var string SQL to count the number of questions matching the current
|
|
|
|
* search conditions.
|
|
|
|
*/
|
2014-03-11 09:16:33 -05:00
|
|
|
protected $countsql;
|
2019-09-27 13:57:29 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @var string SQL to actually load the question data to display.
|
|
|
|
*/
|
2014-03-11 09:16:33 -05:00
|
|
|
protected $loadsql;
|
2019-09-27 13:57:29 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @var array params used by $countsql and $loadsql (which currently must be the same).
|
|
|
|
*/
|
2014-03-11 09:16:33 -05:00
|
|
|
protected $sqlparams;
|
2019-09-27 13:57:29 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @var condition[] search conditions.
|
|
|
|
*/
|
2014-03-11 09:16:33 -05:00
|
|
|
protected $searchconditions = array();
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Constructor
|
2018-07-05 16:59:45 +01:00
|
|
|
* @param \question_edit_contexts $contexts
|
|
|
|
* @param \moodle_url $pageurl
|
2014-03-11 09:16:33 -05:00
|
|
|
* @param object $course course settings
|
|
|
|
* @param object $cm (optional) activity settings.
|
|
|
|
*/
|
|
|
|
public function __construct($contexts, $pageurl, $course, $cm = null) {
|
|
|
|
$this->contexts = $contexts;
|
|
|
|
$this->baseurl = $pageurl;
|
|
|
|
$this->course = $course;
|
|
|
|
$this->cm = $cm;
|
|
|
|
|
|
|
|
// 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();
|
2019-09-27 13:57:29 +01:00
|
|
|
$this->init_search_conditions();
|
2014-03-11 09:16:33 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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',
|
2019-10-02 13:51:48 +01:00
|
|
|
'question_name_idnumber_tags_column',
|
|
|
|
'edit_action_column', 'copy_action_column', 'tags_action_column',
|
|
|
|
'preview_action_column', 'delete_action_column', 'export_xml_action_column',
|
|
|
|
'creator_name_column', 'modifier_name_column', 'edit_menu_column');
|
2014-03-11 09:16:33 -05:00
|
|
|
} 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 = '') {
|
2019-10-02 13:51:48 +01:00
|
|
|
// If we are using the edit menu column, allow it to absorb all the actions.
|
|
|
|
foreach ($wanted as $column) {
|
|
|
|
if ($column instanceof edit_menu_column) {
|
|
|
|
$wanted = $column->claim_menuable_columns($wanted);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Now split columns into real columns and rows.
|
2014-03-11 09:16:33 -05:00
|
|
|
$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
|
2019-09-27 13:57:29 +01:00
|
|
|
* breaking it up, validating the bits that are present, and returning them.
|
2014-03-11 09:16:33 -05:00
|
|
|
* If there is no subsort, then $subsort is returned as ''.
|
2019-09-27 13:57:29 +01:00
|
|
|
*
|
|
|
|
* @param string $sort the sort parameter to process.
|
2014-03-11 09:16:33 -05:00
|
|
|
* @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.
|
2019-09-27 13:57:29 +01:00
|
|
|
list($colname) = $this->parse_subsort($sort);
|
2014-03-11 09:16:33 -05:00
|
|
|
$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() {
|
2019-10-15 20:22:12 +08:00
|
|
|
return array(
|
|
|
|
'core_question\bank\question_type_column' => 1,
|
|
|
|
'core_question\bank\question_name_idnumber_tags_column-name' => 1
|
|
|
|
);
|
2014-03-11 09:16:33 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-09-27 13:57:29 +01:00
|
|
|
* @param string $sort a column or column_subsort name.
|
2014-03-11 09:16:33 -05:00
|
|
|
* @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.
|
2019-09-27 13:57:29 +01:00
|
|
|
*
|
2014-03-11 09:16:33 -05:00
|
|
|
* @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
|
2019-09-27 13:57:29 +01:00
|
|
|
*
|
|
|
|
* @param \stdClass $category no longer used.
|
2014-03-11 09:16:33 -05:00
|
|
|
* @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() {
|
|
|
|
// 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);
|
|
|
|
}
|
|
|
|
|
2019-09-27 13:57:29 +01:00
|
|
|
/**
|
|
|
|
* Load the questions we need to display.
|
|
|
|
*
|
|
|
|
* @param int $page page to display.
|
|
|
|
* @param int $perpage number of questions per page.
|
|
|
|
* @return \moodle_recordset questionid => data about each question.
|
|
|
|
*/
|
2014-03-11 09:16:33 -05:00
|
|
|
protected function load_page_questions($page, $perpage) {
|
|
|
|
global $DB;
|
|
|
|
$questions = $DB->get_recordset_sql($this->loadsql, $this->sqlparams, $page * $perpage, $perpage);
|
2019-08-29 15:41:00 +01:00
|
|
|
if (empty($questions)) {
|
2017-10-11 12:16:12 +01:00
|
|
|
$questions->close();
|
2019-08-29 15:41:00 +01:00
|
|
|
// No questions on this page. Reset to page 0.
|
2014-03-11 09:16:33 -05:00
|
|
|
$questions = $DB->get_recordset_sql($this->loadsql, $this->sqlparams, 0, $perpage);
|
|
|
|
}
|
|
|
|
return $questions;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function base_url() {
|
|
|
|
return $this->baseurl;
|
|
|
|
}
|
|
|
|
|
2019-10-02 13:51:48 +01:00
|
|
|
/**
|
|
|
|
* Get the URL for editing a question as a {@link \moodle_url}.
|
|
|
|
*
|
|
|
|
* @param int $questionid the question id.
|
|
|
|
* @return \moodle_url the URL, HTML-escaped.
|
|
|
|
*/
|
|
|
|
public function edit_question_moodle_url($questionid) {
|
|
|
|
return new \moodle_url($this->editquestionurl, ['id' => $questionid]);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the URL for editing a question as a HTML-escaped string.
|
|
|
|
*
|
|
|
|
* @param int $questionid the question id.
|
|
|
|
* @return string the URL, HTML-escaped.
|
|
|
|
*/
|
2014-03-11 09:16:33 -05:00
|
|
|
public function edit_question_url($questionid) {
|
2019-10-02 13:51:48 +01:00
|
|
|
return $this->edit_question_moodle_url($questionid)->out();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the URL for duplicating a question as a {@link \moodle_url}.
|
|
|
|
*
|
|
|
|
* @param int $questionid the question id.
|
|
|
|
* @return \moodle_url the URL.
|
|
|
|
*/
|
|
|
|
public function copy_question_moodle_url($questionid) {
|
|
|
|
return new \moodle_url($this->editquestionurl, ['id' => $questionid, 'makecopy' => 1]);
|
2014-03-11 09:16:33 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the URL for duplicating a given question.
|
|
|
|
* @param int $questionid the question id.
|
2019-09-27 13:57:29 +01:00
|
|
|
* @return string the URL, HTML-escaped.
|
2014-03-11 09:16:33 -05:00
|
|
|
*/
|
|
|
|
public function copy_question_url($questionid) {
|
2019-10-02 13:51:48 +01:00
|
|
|
return $this->copy_question_moodle_url($questionid)->out();
|
2014-03-11 09:16:33 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the context we are displaying the question bank for.
|
2019-09-27 13:57:29 +01:00
|
|
|
* @return \context context object.
|
2014-03-11 09:16:33 -05:00
|
|
|
*/
|
|
|
|
public function get_most_specific_context() {
|
|
|
|
return $this->contexts->lowest();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the URL to preview a question.
|
2019-09-27 13:57:29 +01:00
|
|
|
* @param \stdClass $questiondata the data defining the question.
|
|
|
|
* @return \moodle_url the URL.
|
2014-03-11 09:16:33 -05:00
|
|
|
*/
|
|
|
|
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
|
2019-09-27 13:57:29 +01:00
|
|
|
*
|
|
|
|
* @param string $tabname question bank edit tab name, for permission checking.
|
|
|
|
* @param int $page the page number to show.
|
|
|
|
* @param int $perpage the number of questions per page to show.
|
|
|
|
* @param string $cat 'categoryid,contextid'.
|
|
|
|
* @param int $recurse Whether to include subcategories.
|
|
|
|
* @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 $tagids current list of selected tags.
|
2014-03-11 09:16:33 -05:00
|
|
|
*/
|
|
|
|
public function display($tabname, $page, $perpage, $cat,
|
2018-02-02 03:52:22 +00:00
|
|
|
$recurse, $showhidden, $showquestiontext, $tagids = []) {
|
2019-03-06 10:25:28 +08:00
|
|
|
global $PAGE, $CFG;
|
2014-03-11 09:16:33 -05:00
|
|
|
|
|
|
|
if ($this->process_actions_needing_ui()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
$editcontexts = $this->contexts->having_one_edit_tab_cap($tabname);
|
2019-09-27 13:57:29 +01:00
|
|
|
list(, $contextid) = explode(',', $cat);
|
2018-02-02 03:52:22 +00:00
|
|
|
$catcontext = \context::instance_by_id($contextid);
|
2018-03-01 11:56:25 +08:00
|
|
|
$thiscontext = $this->get_most_specific_context();
|
2014-03-11 09:16:33 -05:00
|
|
|
// Category selection form.
|
2018-02-02 03:47:44 +00:00
|
|
|
$this->display_question_bank_header();
|
2019-03-06 10:25:28 +08:00
|
|
|
|
|
|
|
// Display tag filter if usetags setting is enabled.
|
|
|
|
if ($CFG->usetags) {
|
|
|
|
array_unshift($this->searchconditions,
|
|
|
|
new \core_question\bank\search\tag_condition([$catcontext, $thiscontext], $tagids));
|
|
|
|
$PAGE->requires->js_call_amd('core_question/edit_tags', 'init', ['#questionscontainer']);
|
|
|
|
}
|
|
|
|
|
2014-03-11 09:16:33 -05:00
|
|
|
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.
|
2018-02-02 03:47:44 +00:00
|
|
|
$this->display_question_list($editcontexts,
|
2014-03-11 09:16:33 -05:00
|
|
|
$this->baseurl, $cat, $this->cm,
|
|
|
|
null, $page, $perpage, $showhidden, $showquestiontext,
|
|
|
|
$this->contexts->having_cap('moodle/question:add'));
|
2018-01-31 13:46:30 +08:00
|
|
|
|
2014-03-11 09:16:33 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
protected function print_choose_category_message($categoryandcontext) {
|
|
|
|
echo "<p style=\"text-align:center;\"><b>";
|
|
|
|
print_string('selectcategoryabove', 'question');
|
|
|
|
echo "</b></p>";
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function get_current_category($categoryandcontext) {
|
|
|
|
global $DB, $OUTPUT;
|
|
|
|
list($categoryid, $contextid) = explode(',', $categoryandcontext);
|
|
|
|
if (!$categoryid) {
|
|
|
|
$this->print_choose_category_message($categoryandcontext);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$category = $DB->get_record('question_categories',
|
|
|
|
array('id' => $categoryid, 'contextid' => $contextid))) {
|
|
|
|
echo $OUTPUT->box_start('generalbox questionbank');
|
|
|
|
echo $OUTPUT->notification('Category not found!');
|
|
|
|
echo $OUTPUT->box_end();
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $category;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* prints category information
|
2019-09-27 13:57:29 +01:00
|
|
|
* @param \stdClass $category the category row from the database.
|
2014-03-11 09:16:33 -05:00
|
|
|
* @deprecated since Moodle 2.7 MDL-40313.
|
|
|
|
* @see \core_question\bank\search\condition
|
|
|
|
* @todo MDL-41978 This will be deleted in Moodle 2.8
|
|
|
|
*/
|
|
|
|
protected function print_category_info($category) {
|
|
|
|
$formatoptions = new \stdClass();
|
|
|
|
$formatoptions->noclean = true;
|
|
|
|
$formatoptions->overflowdiv = true;
|
|
|
|
echo '<div class="boxaligncenter">';
|
|
|
|
echo format_text($category->info, $category->infoformat, $formatoptions, $this->course->id);
|
|
|
|
echo "</div>\n";
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Prints a form to choose categories
|
|
|
|
* @deprecated since Moodle 2.7 MDL-40313.
|
|
|
|
* @see \core_question\bank\search\condition
|
|
|
|
* @todo MDL-41978 This will be deleted in Moodle 2.8
|
|
|
|
*/
|
|
|
|
protected function display_category_form($contexts, $pageurl, $current) {
|
|
|
|
global $OUTPUT;
|
|
|
|
|
|
|
|
debugging('display_category_form() is deprecated, please use ' .
|
|
|
|
'\core_question\bank\search\condition instead.', DEBUG_DEVELOPER);
|
|
|
|
// Get all the existing categories now.
|
|
|
|
echo '<div class="choosecategory">';
|
|
|
|
$catmenu = question_category_options($contexts, false, 0, true);
|
|
|
|
|
|
|
|
$select = new \single_select($this->baseurl, 'category', $catmenu, $current, null, 'catmenu');
|
|
|
|
$select->set_label(get_string('selectacategory', 'question'));
|
|
|
|
echo $OUTPUT->render($select);
|
|
|
|
echo "</div>\n";
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Display the options form.
|
|
|
|
* @param bool $recurse no longer used.
|
|
|
|
* @param bool $showhidden no longer used.
|
|
|
|
* @param bool $showquestiontext whether to show the question text.
|
|
|
|
* @deprecated since Moodle 2.7 MDL-40313.
|
|
|
|
* @see display_options_form
|
|
|
|
* @todo MDL-41978 This will be deleted in Moodle 2.8
|
|
|
|
* @see \core_question\bank\search\condition
|
|
|
|
*/
|
|
|
|
protected function display_options($recurse, $showhidden, $showquestiontext) {
|
|
|
|
debugging('display_options() is deprecated, please use display_options_form instead.', DEBUG_DEVELOPER);
|
2019-09-27 13:57:29 +01:00
|
|
|
$this->display_options_form($showquestiontext);
|
2014-03-11 09:16:33 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Print a single option checkbox.
|
|
|
|
* @deprecated since Moodle 2.7 MDL-40313.
|
|
|
|
* @see \core_question\bank\search\condition
|
|
|
|
* @see html_writer::checkbox
|
|
|
|
* @todo MDL-41978 This will be deleted in Moodle 2.8
|
|
|
|
*/
|
|
|
|
protected function display_category_form_checkbox($name, $value, $label) {
|
|
|
|
debugging('display_category_form_checkbox() is deprecated, ' .
|
|
|
|
'please use \core_question\bank\search\condition instead.', DEBUG_DEVELOPER);
|
|
|
|
echo '<div><input type="hidden" id="' . $name . '_off" name="' . $name . '" value="0" />';
|
|
|
|
echo '<input type="checkbox" id="' . $name . '_on" name="' . $name . '" value="1"';
|
|
|
|
if ($value) {
|
|
|
|
echo ' checked="checked"';
|
|
|
|
}
|
|
|
|
echo ' onchange="getElementById(\'displayoptions\').submit(); return true;" />';
|
|
|
|
echo '<label for="' . $name . '_on">' . $label . '</label>';
|
|
|
|
echo "</div>\n";
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Display the form with options for which questions are displayed and how they are displayed.
|
|
|
|
* @param bool $showquestiontext Display the text of the question within the list.
|
2019-09-27 13:57:29 +01:00
|
|
|
* @param string $scriptpath path to the script displaying this page.
|
2014-07-02 14:26:59 +01:00
|
|
|
* @param bool $showtextoption whether to include the 'Show question text' checkbox.
|
2014-03-11 09:16:33 -05:00
|
|
|
*/
|
2014-07-02 14:26:59 +01:00
|
|
|
protected function display_options_form($showquestiontext, $scriptpath = '/question/edit.php',
|
|
|
|
$showtextoption = true) {
|
2014-03-11 09:16:33 -05:00
|
|
|
global $PAGE;
|
|
|
|
|
2014-07-02 14:26:59 +01:00
|
|
|
echo \html_writer::start_tag('form', array('method' => 'get',
|
|
|
|
'action' => new \moodle_url($scriptpath), 'id' => 'displayoptions'));
|
|
|
|
echo \html_writer::start_div();
|
2018-02-02 03:52:22 +00:00
|
|
|
|
|
|
|
$excludes = array('recurse', 'showhidden', 'qbshowtext');
|
|
|
|
// If the URL contains any tags then we need to prevent them
|
|
|
|
// being added to the form as hidden elements because the tags
|
|
|
|
// are managed separately.
|
|
|
|
if ($this->baseurl->param('qtagids[0]')) {
|
|
|
|
$index = 0;
|
|
|
|
while ($this->baseurl->param("qtagids[{$index}]")) {
|
|
|
|
$excludes[] = "qtagids[{$index}]";
|
|
|
|
$index++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
echo \html_writer::input_hidden_params($this->baseurl, $excludes);
|
2014-03-11 09:16:33 -05:00
|
|
|
|
|
|
|
foreach ($this->searchconditions as $searchcondition) {
|
2019-09-27 13:57:29 +01:00
|
|
|
echo $searchcondition->display_options();
|
2014-03-11 09:16:33 -05:00
|
|
|
}
|
2014-07-02 14:26:59 +01:00
|
|
|
if ($showtextoption) {
|
|
|
|
$this->display_showtext_checkbox($showquestiontext);
|
|
|
|
}
|
2014-03-11 09:16:33 -05:00
|
|
|
$this->display_advanced_search_form();
|
2014-07-02 14:26:59 +01:00
|
|
|
$go = \html_writer::empty_tag('input', array('type' => 'submit', 'value' => get_string('go')));
|
|
|
|
echo \html_writer::tag('noscript', \html_writer::div($go), array('class' => 'inline'));
|
|
|
|
echo \html_writer::end_div();
|
|
|
|
echo \html_writer::end_tag('form');
|
2014-03-11 09:16:33 -05:00
|
|
|
$PAGE->requires->yui_module('moodle-question-searchform', 'M.question.searchform.init');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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) {
|
2019-09-27 13:57:29 +01:00
|
|
|
echo $searchcondition->display_options_adv();
|
2014-03-11 09:16:33 -05:00
|
|
|
}
|
|
|
|
print_collapsible_region_end();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Display the checkbox UI for toggling the display of the question text in the list.
|
|
|
|
* @param bool $showquestiontext the current or default value for whether to display the text.
|
|
|
|
*/
|
|
|
|
protected function display_showtext_checkbox($showquestiontext) {
|
|
|
|
echo '<div>';
|
|
|
|
echo \html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'qbshowtext',
|
|
|
|
'value' => 0, 'id' => 'qbshowtext_off'));
|
2019-10-23 03:35:13 +02:00
|
|
|
echo \html_writer::checkbox('qbshowtext', '1', $showquestiontext, ' ' . get_string('showquestiontext', 'question'),
|
|
|
|
array('id' => 'qbshowtext_on', 'class' => 'searchoptions mr-1'));
|
2014-03-11 09:16:33 -05:00
|
|
|
echo "</div>\n";
|
|
|
|
}
|
|
|
|
|
2018-02-02 03:47:44 +00:00
|
|
|
/**
|
|
|
|
* Display the header element for the question bank.
|
|
|
|
*/
|
|
|
|
protected function display_question_bank_header() {
|
|
|
|
global $OUTPUT;
|
|
|
|
echo $OUTPUT->heading(get_string('questionbank', 'question'), 2);
|
|
|
|
}
|
|
|
|
|
2014-03-11 09:16:33 -05:00
|
|
|
protected function create_new_question_form($category, $canadd) {
|
|
|
|
echo '<div class="createnewquestion">';
|
|
|
|
if ($canadd) {
|
|
|
|
create_new_question_button($category->id, $this->editquestionurl->params(),
|
|
|
|
get_string('createnewquestion', 'question'));
|
|
|
|
} else {
|
|
|
|
print_string('nopermissionadd', 'question');
|
|
|
|
}
|
|
|
|
echo '</div>';
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Prints the table of questions in a category with interactions
|
|
|
|
*
|
|
|
|
* @param array $contexts Not used!
|
2019-09-27 13:57:29 +01:00
|
|
|
* @param \moodle_url $pageurl The URL to reload this page.
|
2014-03-11 09:16:33 -05:00
|
|
|
* @param string $categoryandcontext 'categoryID,contextID'.
|
2019-09-27 13:57:29 +01:00
|
|
|
* @param \stdClass $cm Not used!
|
|
|
|
* @param int $recurse Whether to include subcategories.
|
2014-03-11 09:16:33 -05:00
|
|
|
* @param int $page The number of the page to be displayed
|
|
|
|
* @param int $perpage Number of questions to show per page
|
2019-09-27 13:57:29 +01:00
|
|
|
* @param bool $showhidden Not used! This is now controlled in a different way.
|
|
|
|
* @param bool $showquestiontext Not used! This is now controlled in a different way.
|
2014-03-11 09:16:33 -05:00
|
|
|
* @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()) {
|
2019-09-27 13:57:29 +01:00
|
|
|
global $OUTPUT;
|
2014-03-11 09:16:33 -05:00
|
|
|
|
2015-07-08 15:49:10 -04:00
|
|
|
// This function can be moderately slow with large question counts and may time out.
|
|
|
|
// We probably do not want to raise it to unlimited, so randomly picking 5 minutes.
|
|
|
|
// Note: We do not call this in the loop because quiz ob_ captures this function (see raise() PHP doc).
|
|
|
|
\core_php_time_limit::raise(300);
|
|
|
|
|
2014-03-11 09:16:33 -05:00
|
|
|
$category = $this->get_current_category($categoryandcontext);
|
|
|
|
|
|
|
|
list($categoryid, $contextid) = explode(',', $categoryandcontext);
|
|
|
|
$catcontext = \context::instance_by_id($contextid);
|
|
|
|
|
|
|
|
$canadd = has_capability('moodle/question:add', $catcontext);
|
|
|
|
|
|
|
|
$this->create_new_question_form($category, $canadd);
|
|
|
|
|
|
|
|
$this->build_query();
|
|
|
|
$totalnumber = $this->get_question_count();
|
|
|
|
if ($totalnumber == 0) {
|
|
|
|
return;
|
|
|
|
}
|
2019-08-29 15:41:00 +01:00
|
|
|
$questionsrs = $this->load_page_questions($page, $perpage);
|
|
|
|
$questions = [];
|
|
|
|
foreach ($questionsrs as $question) {
|
|
|
|
$questions[$question->id] = $question;
|
|
|
|
}
|
|
|
|
$questionsrs->close();
|
|
|
|
foreach ($this->requiredcolumns as $name => $column) {
|
|
|
|
$column->load_additional_data($questions);
|
|
|
|
}
|
2014-03-11 09:16:33 -05:00
|
|
|
|
|
|
|
echo '<div class="categorypagingbarcontainer">';
|
2019-09-27 13:57:29 +01:00
|
|
|
$pageingurl = new \moodle_url('edit.php', $pageurl->params());
|
2014-03-11 09:16:33 -05:00
|
|
|
$pagingbar = new \paging_bar($totalnumber, $page, $perpage, $pageingurl);
|
|
|
|
$pagingbar->pagevar = 'qpage';
|
|
|
|
echo $OUTPUT->render($pagingbar);
|
|
|
|
echo '</div>';
|
|
|
|
|
|
|
|
echo '<form method="post" action="edit.php">';
|
|
|
|
echo '<fieldset class="invisiblefieldset" style="display: block;">';
|
|
|
|
echo '<input type="hidden" name="sesskey" value="'.sesskey().'" />';
|
|
|
|
echo \html_writer::input_hidden_params($this->baseurl);
|
|
|
|
|
2018-01-31 13:46:30 +08:00
|
|
|
echo '<div class="categoryquestionscontainer" id="questionscontainer">';
|
2014-03-11 09:16:33 -05:00
|
|
|
$this->start_table();
|
|
|
|
$rowcount = 0;
|
|
|
|
foreach ($questions as $question) {
|
|
|
|
$this->print_table_row($question, $rowcount);
|
|
|
|
$rowcount += 1;
|
|
|
|
}
|
|
|
|
$this->end_table();
|
|
|
|
echo "</div>\n";
|
|
|
|
|
|
|
|
echo '<div class="categorypagingbarcontainer pagingbottom">';
|
|
|
|
echo $OUTPUT->render($pagingbar);
|
|
|
|
if ($totalnumber > DEFAULT_QUESTIONS_PER_PAGE) {
|
|
|
|
if ($perpage == DEFAULT_QUESTIONS_PER_PAGE) {
|
2015-07-08 17:17:02 -04:00
|
|
|
$url = new \moodle_url('edit.php', array_merge($pageurl->params(),
|
|
|
|
array('qperpage' => MAXIMUM_QUESTIONS_PER_PAGE)));
|
|
|
|
if ($totalnumber > MAXIMUM_QUESTIONS_PER_PAGE) {
|
|
|
|
$showall = '<a href="'.$url.'">'.get_string('showperpage', 'moodle', MAXIMUM_QUESTIONS_PER_PAGE).'</a>';
|
|
|
|
} else {
|
|
|
|
$showall = '<a href="'.$url.'">'.get_string('showall', 'moodle', $totalnumber).'</a>';
|
|
|
|
}
|
2014-03-11 09:16:33 -05:00
|
|
|
} else {
|
|
|
|
$url = new \moodle_url('edit.php', array_merge($pageurl->params(),
|
|
|
|
array('qperpage' => DEFAULT_QUESTIONS_PER_PAGE)));
|
|
|
|
$showall = '<a href="'.$url.'">'.get_string('showperpage', 'moodle', DEFAULT_QUESTIONS_PER_PAGE).'</a>';
|
|
|
|
}
|
2014-06-30 19:09:54 +01:00
|
|
|
echo "<div class='paging'>{$showall}</div>";
|
2014-03-11 09:16:33 -05:00
|
|
|
}
|
|
|
|
echo '</div>';
|
|
|
|
|
MDL-43089 quiz: improved interface for building quizzes
This commit is actually the joint work of Mahmoud Kassaei, Colin
Chambers and Tim Hunt from The Open University. We could only use one
persons name for the commit, and this time Colin gets the credit/blame.
The goal of this work was to increase usability, and also clean up
the page enough that it will be possible to add new features in future.
Display of mod/quiz/edit.php is now entirely generated by
mod_quiz\output\edit_renderer. This uses a helper class
mod_quiz\structure to provide details of the structure of the quiz, and
mod_quiz\repaginate to alter that structure. (Acutally, there are still
some modification methods on mod_quiz\structure. Expect that to be
cleaned up in future.)
The new code uses much more ajax, and there are new scripts
mod/quiz/edit_rest.php and mod/quiz/repaginate.php to handle this.
(Again, don't be surprised if those two scripts get merged in future.)
Also questionbank.ajax.php (which may, in future, be made more generic,
and moved into the core question bank code.)
Most of the new JavaScript code has intentionally copied the way things
are done when editing activities on the course page.
As a result of this, mod/quiz/editlib.php is now much shorter than it
was. (In future, expect the remaining code in here to move into
mod/quiz/classes.)
2013-12-05 16:18:24 +00:00
|
|
|
$this->display_bottom_controls($totalnumber, $recurse, $category, $catcontext, $addcontexts);
|
|
|
|
|
|
|
|
echo '</fieldset>';
|
|
|
|
echo "</form>\n";
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Display the controls at the bottom of the list of questions.
|
|
|
|
* @param int $totalnumber Total number of questions that might be shown (if it was not for paging).
|
|
|
|
* @param bool $recurse Whether to include subcategories.
|
2019-09-27 13:57:29 +01:00
|
|
|
* @param \stdClass $category The question_category row from the database.
|
|
|
|
* @param \context $catcontext The context of the category being displayed.
|
MDL-43089 quiz: improved interface for building quizzes
This commit is actually the joint work of Mahmoud Kassaei, Colin
Chambers and Tim Hunt from The Open University. We could only use one
persons name for the commit, and this time Colin gets the credit/blame.
The goal of this work was to increase usability, and also clean up
the page enough that it will be possible to add new features in future.
Display of mod/quiz/edit.php is now entirely generated by
mod_quiz\output\edit_renderer. This uses a helper class
mod_quiz\structure to provide details of the structure of the quiz, and
mod_quiz\repaginate to alter that structure. (Acutally, there are still
some modification methods on mod_quiz\structure. Expect that to be
cleaned up in future.)
The new code uses much more ajax, and there are new scripts
mod/quiz/edit_rest.php and mod/quiz/repaginate.php to handle this.
(Again, don't be surprised if those two scripts get merged in future.)
Also questionbank.ajax.php (which may, in future, be made more generic,
and moved into the core question bank code.)
Most of the new JavaScript code has intentionally copied the way things
are done when editing activities on the course page.
As a result of this, mod/quiz/editlib.php is now much shorter than it
was. (In future, expect the remaining code in here to move into
mod/quiz/classes.)
2013-12-05 16:18:24 +00:00
|
|
|
* @param array $addcontexts contexts where the user is allowed to add new questions.
|
|
|
|
*/
|
|
|
|
protected function display_bottom_controls($totalnumber, $recurse, $category, \context $catcontext, array $addcontexts) {
|
|
|
|
$caneditall = has_capability('moodle/question:editall', $catcontext);
|
|
|
|
$canuseall = has_capability('moodle/question:useall', $catcontext);
|
|
|
|
$canmoveall = has_capability('moodle/question:moveall', $catcontext);
|
|
|
|
|
2014-03-11 09:16:33 -05:00
|
|
|
echo '<div class="modulespecificbuttonscontainer">';
|
|
|
|
if ($caneditall || $canmoveall || $canuseall) {
|
|
|
|
echo '<strong> '.get_string('withselected', 'question').':</strong><br />';
|
|
|
|
|
|
|
|
// Print delete and move selected question.
|
|
|
|
if ($caneditall) {
|
2019-06-21 17:10:25 +08:00
|
|
|
echo \html_writer::empty_tag('input', [
|
|
|
|
'type' => 'submit',
|
|
|
|
'class' => 'btn btn-secondary mr-1',
|
|
|
|
'name' => 'deleteselected',
|
|
|
|
'value' => get_string('delete'),
|
|
|
|
'data-action' => 'toggle',
|
|
|
|
'data-togglegroup' => 'qbank',
|
|
|
|
'data-toggle' => 'action',
|
|
|
|
'disabled' => true,
|
|
|
|
]);
|
2014-03-11 09:16:33 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
if ($canmoveall && count($addcontexts)) {
|
2019-06-21 17:10:25 +08:00
|
|
|
echo \html_writer::empty_tag('input', [
|
|
|
|
'type' => 'submit',
|
|
|
|
'class' => 'btn btn-secondary mr-1',
|
|
|
|
'name' => 'move',
|
|
|
|
'value' => get_string('moveto', 'question'),
|
|
|
|
'data-action' => 'toggle',
|
|
|
|
'data-togglegroup' => 'qbank',
|
|
|
|
'data-toggle' => 'action',
|
|
|
|
'disabled' => true,
|
|
|
|
]);
|
2014-06-30 19:09:54 +01:00
|
|
|
question_category_select_menu($addcontexts, false, 0, "{$category->id},{$category->contextid}");
|
2014-03-11 09:16:33 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
echo "</div>\n";
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function start_table() {
|
|
|
|
echo '<table id="categoryquestions">' . "\n";
|
|
|
|
echo "<thead>\n";
|
|
|
|
$this->print_table_headers();
|
|
|
|
echo "</thead>\n";
|
|
|
|
echo "<tbody>\n";
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function end_table() {
|
|
|
|
echo "</tbody>\n";
|
|
|
|
echo "</table>\n";
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function print_table_headers() {
|
|
|
|
echo "<tr>\n";
|
|
|
|
foreach ($this->visiblecolumns as $column) {
|
|
|
|
$column->display_header();
|
|
|
|
}
|
|
|
|
echo "</tr>\n";
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function get_row_classes($question, $rowcount) {
|
|
|
|
$classes = array();
|
|
|
|
if ($question->hidden) {
|
|
|
|
$classes[] = 'dimmed_text';
|
|
|
|
}
|
|
|
|
if ($question->id == $this->lastchangedid) {
|
|
|
|
$classes[] = 'highlight';
|
|
|
|
}
|
|
|
|
$classes[] = 'r' . ($rowcount % 2);
|
|
|
|
return $classes;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function print_table_row($question, $rowcount) {
|
|
|
|
$rowclasses = implode(' ', $this->get_row_classes($question, $rowcount));
|
|
|
|
if ($rowclasses) {
|
|
|
|
echo '<tr class="' . $rowclasses . '">' . "\n";
|
|
|
|
} else {
|
|
|
|
echo "<tr>\n";
|
|
|
|
}
|
|
|
|
foreach ($this->visiblecolumns as $column) {
|
|
|
|
$column->display($question, $rowclasses);
|
|
|
|
}
|
|
|
|
echo "</tr>\n";
|
|
|
|
foreach ($this->extrarows as $row) {
|
|
|
|
$row->display($question, $rowclasses);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public function process_actions() {
|
2019-09-27 13:57:29 +01:00
|
|
|
global $DB;
|
2014-03-11 09:16:33 -05:00
|
|
|
// 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);
|
|
|
|
$questions = $DB->get_records_sql("
|
|
|
|
SELECT q.*, c.contextid
|
|
|
|
FROM {question} q
|
|
|
|
JOIN {question_categories} c ON c.id = q.category
|
2014-06-30 19:09:54 +01:00
|
|
|
WHERE q.id {$usql}", $params);
|
2014-03-11 09:16:33 -05:00
|
|
|
foreach ($questions as $question) {
|
|
|
|
question_require_capability_on($question, 'move');
|
|
|
|
}
|
|
|
|
question_move_questions_to_category($questionids, $tocategory->id);
|
|
|
|
redirect($this->baseurl->out(false,
|
2014-06-30 19:09:54 +01:00
|
|
|
array('category' => "{$tocategoryid},{$contextid}")));
|
2014-03-11 09:16:33 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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.
|
2015-03-31 22:57:51 -07:00
|
|
|
\question_bank::notify_question_edited($unhide);
|
2014-03-11 09:16:33 -05:00
|
|
|
|
|
|
|
redirect($this->baseurl);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public function process_actions_needing_ui() {
|
|
|
|
global $DB, $OUTPUT;
|
|
|
|
if (optional_param('deleteselected', false, PARAM_BOOL)) {
|
|
|
|
// Make a list of all the questions that are selected.
|
|
|
|
$rawquestions = $_REQUEST; // This code is called by both POST forms and GET links, so cannot use data_submitted.
|
|
|
|
$questionlist = ''; // comma separated list of ids of questions to be deleted
|
|
|
|
$questionnames = ''; // string with names of questions separated by <br /> with
|
|
|
|
// an asterix in front of those that are in use
|
|
|
|
$inuse = false; // set to true if at least one of the questions is in use
|
|
|
|
foreach ($rawquestions as $key => $value) { // Parse input for question ids.
|
|
|
|
if (preg_match('!^q([0-9]+)$!', $key, $matches)) {
|
|
|
|
$key = $matches[1];
|
|
|
|
$questionlist .= $key.',';
|
2018-03-26 11:31:24 +08:00
|
|
|
question_require_capability_on((int)$key, 'edit');
|
2014-03-11 09:16:33 -05:00
|
|
|
if (questions_in_use(array($key))) {
|
|
|
|
$questionnames .= '* ';
|
|
|
|
$inuse = true;
|
|
|
|
}
|
|
|
|
$questionnames .= $DB->get_field('question', 'name', array('id' => $key)) . '<br />';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!$questionlist) { // No questions were selected.
|
|
|
|
redirect($this->baseurl);
|
|
|
|
}
|
|
|
|
$questionlist = rtrim($questionlist, ',');
|
|
|
|
|
|
|
|
// Add an explanation about questions in use.
|
|
|
|
if ($inuse) {
|
|
|
|
$questionnames .= '<br />'.get_string('questionsinuse', 'question');
|
|
|
|
}
|
|
|
|
$baseurl = new \moodle_url('edit.php', $this->baseurl->params());
|
|
|
|
$deleteurl = new \moodle_url($baseurl, array('deleteselected' => $questionlist, 'confirm' => md5($questionlist),
|
|
|
|
'sesskey' => sesskey()));
|
|
|
|
|
2015-08-25 22:05:33 +08:00
|
|
|
$continue = new \single_button($deleteurl, get_string('delete'), 'post');
|
|
|
|
echo $OUTPUT->confirm(get_string('deletequestionscheck', 'question', $questionnames), $continue, $baseurl);
|
2014-03-11 09:16:33 -05:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
2019-09-27 13:57:29 +01:00
|
|
|
|
|
|
|
return false;
|
2014-03-11 09:16:33 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add another search control to this view.
|
2019-09-27 13:57:29 +01:00
|
|
|
* @param condition $searchcondition the condition to add.
|
2014-03-11 09:16:33 -05:00
|
|
|
*/
|
|
|
|
public function add_searchcondition($searchcondition) {
|
|
|
|
$this->searchconditions[] = $searchcondition;
|
|
|
|
}
|
|
|
|
}
|