Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.
<?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/>.

/**
 * Class to print a view of the question bank.
 *
 * @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
 */

namespace core_question\local\bank;

< use core_plugin_manager; < use core_question\bank\search\condition; < use core_question\local\statistics\statistics_bulk_loader; < use qbank_columnsortorder\column_manager; < use qbank_editquestion\editquestion_helper; <
defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/question/editlib.php');
> use core\plugininfo\qbank; /** > use core\output\datafilter; * This class prints a view of the question bank. > use core_plugin_manager; * > use core_question\local\bank\condition; * including > use core_question\local\statistics\statistics_bulk_loader; * + Some controls to allow users to to select what is displayed. > use core_question\output\question_bank_filter_ui; * + A list of questions as a table. > use core_question\local\bank\column_manager_base; * + Further controls to do things with the questions. > use qbank_deletequestion\hidden_condition; * > use qbank_editquestion\editquestion_helper; * This class gives a basic view, and provides plenty of hooks where subclasses > use qbank_managecategories\category_condition; * 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 * @author 2021 Safat Shahin <safatshahin@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class view { /** * Maximum number of sorts allowed. */ const MAX_SORTS = 3; /** * @var \moodle_url base URL for the current page. Used as the * basis for making URLs for actions that reload the page. */ protected $baseurl; /** * @var \moodle_url used as a basis for URLs that edit a question. */ protected $editquestionurl; /** * @var \core_question\local\bank\question_edit_contexts */
< protected $contexts;
> public $contexts;
/** * @var object|\cm_info|null if we are in a module context, the cm. */ public $cm; /** * @var object the course we are within. */ public $course; /** * @var column_base[] these are all the 'columns' that are * part of the display. Array keys are the class name. */ protected $requiredcolumns; /**
> * @var question_action_base[] these are all the actions that can be displayed in a question's action menu. * @var column_base[] these are the 'columns' that are > * * actually displayed as a column, in order. Array keys are the class name. > * Array keys are the class name. */ > */ protected $visiblecolumns; > protected $questionactions; > /** > /**
* @var column_base[] these are the 'columns' that are
> * common to the question bank. * actually displayed as an additional row (e.g. question text), in order. > */ * Array keys are the class name. > protected $corequestionbankcolumns; */ > protected $extrarows; > /** > * @var column_base[] these are the 'columns' that are
/** * @var array list of column class names for which columns to sort on. */ protected $sort; /** * @var int page size to use (when we are not showing all questions). */ protected $pagesize = DEFAULT_QUESTIONS_PER_PAGE; /** * @var int|null id of the a question to highlight in the list (if present). */ protected $lastchangedid; /** * @var string SQL to count the number of questions matching the current * search conditions. */ protected $countsql; /** * @var string SQL to actually load the question data to display. */ protected $loadsql; /** * @var array params used by $countsql and $loadsql (which currently must be the same). */ protected $sqlparams; /** * @var ?array Stores all the average statistics that this question bank view needs. * * This field gets initialised in {@see display_question_list()}. It is a two dimensional * $this->loadedstatistics[$questionid][$fieldname] = $average value of that statistics for that question. * Column classes in qbank plugins can access these values using {@see get_aggregate_statistic()}. */ protected $loadedstatistics = null; /** * @var condition[] search conditions. */ protected $searchconditions = []; /** * @var string url of the new question page. */ public $returnurl; /**
< * @var bool enable or disable filters while calling the API.
> * @var array $bulkactions to identify the bulk actions for the api.
*/
< public $enablefilters = true;
> public $bulkactions = [];
/**
< * @var array to pass custom filters instead of the specified ones.
> * @var int|null Number of questions.
*/
< public $customfilterobjects = null;
> protected $totalcount = null;
/**
< * @var array $bulkactions to identify the bulk actions for the api.
> * @var array Parameters for the page URL.
*/
< public $bulkactions = [];
> protected $pagevars = []; > > /** > * @var plugin_features_base[] $plugins Plugin feature objects for all enabled qbank plugins. > */ > protected $plugins = []; > > /** > * @var string $component the component the api is used from. > */ > public $component = 'core_question'; > > /** > * @var string $callback name of the callback for the api call via filter js. > */ > public $callback = 'question_data'; > > /** > * @var array $extraparams extra parameters for the extended apis. > */ > public $extraparams = []; > > /** > * @var column_manager_base $columnmanager The column manager, can be overridden by plugins. > */ > protected $columnmanager;
/** * Constructor for view. * * @param \core_question\local\bank\question_edit_contexts $contexts * @param \moodle_url $pageurl * @param object $course course settings
< * @param object $cm (optional) activity settings.
> * @param null $cm (optional) activity settings. > * @param array $params the parameters required to initialize the api. > * @param array $extraparams any extra parameters required by a particular view class.
*/
< public function __construct($contexts, $pageurl, $course, $cm = null) {
> public function __construct($contexts, $pageurl, $course, $cm = null, $params = [], $extraparams = []) {
$this->contexts = $contexts; $this->baseurl = $pageurl; $this->course = $course; $this->cm = $cm;
> $this->extraparams = $extraparams; > // Create the url of the new question page to forward to. > // Default filter condition. $this->returnurl = $pageurl->out_as_local_url(false); > if (!isset($params['filter']) && isset($params['cat'])) { $this->editquestionurl = new \moodle_url('/question/bank/editquestion/question.php', ['returnurl' => $this->returnurl]); > $params['filter'] = []; if ($this->cm !== null) { > [$categoryid, $contextid] = category_condition::validate_category_param($params['cat']); $this->editquestionurl->param('cmid', $this->cm->id); > if (!is_null($categoryid)) { } else { > $category = category_condition::get_category_record($categoryid, $contextid); $this->editquestionurl->param('courseid', $this->course->id); > $params['filter']['category'] = [ } > 'jointype' => category_condition::JOINTYPE_DEFAULT, > 'values' => [$category->id], $this->lastchangedid = optional_param('lastchanged', 0, PARAM_INT); > 'filteroptions' => ['includesubcategories' => false], > ]; // Possibly the heading part can be removed. > } $this->init_columns($this->wanted_columns(), $this->heading_column()); > $params['filter']['hidden'] = [ $this->init_sort(); > 'jointype' => hidden_condition::JOINTYPE_DEFAULT, $this->init_search_conditions(); > 'values' => [0], $this->init_bulk_actions(); > ]; } > $params['jointype'] = datafilter::JOINTYPE_ALL; > } /** > if (!empty($params['filter'])) { * Initialize bulk actions. > $params['filter'] = filter_condition_manager::unpack_filteroptions_param($params['filter']); */ > } protected function init_bulk_actions(): void { > if (isset($params['filter']['jointype'])) { $plugins = \core_component::get_plugin_list_with_class('qbank', 'plugin_feature', 'plugin_feature.php'); > $params['jointype'] = $params['filter']['jointype']; foreach ($plugins as $componentname => $plugin) { > unset($params['filter']['jointype']); if (!\core\plugininfo\qbank::is_plugin_enabled($componentname)) { > }
< $this->lastchangedid = optional_param('lastchanged', 0, PARAM_INT);
> $this->lastchangedid = clean_param($pageurl->param('lastchanged'), PARAM_INT);
}
> $this->init_plugins(); > $this->init_column_manager();
$pluginentrypoint = new $plugin();
> $this->set_pagevars($params);
$bulkactions = $pluginentrypoint->get_bulk_actions();
> $this->init_question_actions();
< $this->init_search_conditions();
< * Initialize bulk actions.
> * Get an array of plugin features objects for all enabled qbank plugins. > * > * @return void
< protected function init_bulk_actions(): void {
> protected function init_plugins(): void {
< foreach ($plugins as $componentname => $plugin) {
> foreach ($plugins as $componentname => $pluginclass) {
"Check out the qbank_bulkmove plugin for a working example.", DEBUG_DEVELOPER);
> $this->plugins[$componentname] = new $pluginclass(); $bulkactions = [$bulkactions]; > } } > // Sort plugin list by component name. > ksort($this->plugins); foreach ($bulkactions as $bulkactionobject) { > }
< $pluginentrypoint = new $plugin(); < $bulkactions = $pluginentrypoint->get_bulk_actions();
> /** > * Allow qbank plugins to override the column manager. > * > * If multiple qbank plugins define a column manager, this will pick the first one sorted alphabetically. > * > * @return void > */ > protected function init_column_manager(): void { > $this->columnmanager = new column_manager_base(); > foreach ($this->plugins as $plugin) { > if ($columnmanager = $plugin->get_column_manager()) { > $this->columnmanager = $columnmanager; > break; > } > } > } > > /** > * Initialize bulk actions. > */ > protected function init_bulk_actions(): void { > foreach ($this->plugins as $componentname => $plugin) { > $bulkactions = $plugin->get_bulk_actions();
'url' => $bulkactionobject->get_bulk_action_url(), 'capabilities' => $bulkactionobject->get_bulk_action_capabilities() ]; }
<
} } /** * Initialize search conditions from plugins * local_*_get_question_bank_search_conditions() must return an array of * \core_question\bank\search\condition objects.
> * */ > * @deprecated Since Moodle 4.3 protected function init_search_conditions(): void { > * @todo Final deprecation on Moodle 4.7 MDL-78090
$searchplugins = get_plugin_list_with_function('local', 'get_question_bank_search_conditions');
> debugging( foreach ($searchplugins as $component => $function) { > 'Function init_search_conditions() has been deprecated, please create a qbank plugin' . foreach ($function($this) as $searchobject) { > 'and implement a filter object instead.', $this->add_searchcondition($searchobject); > DEBUG_DEVELOPER } > );
} } /**
< * Get the list of qbank plugins with available objects for features.
> * Initialise list of menu actions for enabled question bank plugins. > * > * Menu action objects are stored in $this->menuactions, keyed by class name. > * > * @return void > */ > protected function init_question_actions(): void { > $this->questionactions = []; > foreach ($this->plugins as $plugin) { > $menuactions = $plugin->get_question_actions($this); > foreach ($menuactions as $menuaction) { > $this->questionactions[$menuaction::class] = $menuaction; > } > } > } > > /** > * Get class for each question bank columns.
* * @return array */ protected function get_question_bank_plugins(): array { $questionbankclasscolumns = []; $newpluginclasscolumns = []; $corequestionbankcolumns = [
< 'checkbox_column', < 'question_type_column', < 'question_name_idnumber_tags_column', < 'edit_menu_column', < 'edit_action_column', < 'copy_action_column', < 'tags_action_column', < 'preview_action_column', < 'history_action_column', < 'delete_action_column', < 'export_xml_action_column', < 'question_status_column', < 'version_number_column', < 'creator_name_column', < 'comment_count_column'
> 'core_question\local\bank\checkbox_column' . column_base::ID_SEPARATOR . 'checkbox_column', > 'core_question\local\bank\edit_menu_column' . column_base::ID_SEPARATOR . 'edit_menu_column',
];
< if (question_get_display_preference('qbshowtext', 0, PARAM_INT, new \moodle_url(''))) { < $corequestionbankcolumns[] = 'question_text_row'; < }
< foreach ($corequestionbankcolumns as $fullname) { < $shortname = $fullname; < if (class_exists('core_question\\local\\bank\\' . $fullname)) { < $fullname = 'core_question\\local\\bank\\' . $fullname; < $questionbankclasscolumns[$shortname] = new $fullname($this); < } else { < $questionbankclasscolumns[$shortname] = '';
> foreach ($corequestionbankcolumns as $columnid) { > [$columnclass, $columnname] = explode(column_base::ID_SEPARATOR, $columnid, 2); > if (class_exists($columnclass)) { > $questionbankclasscolumns[$columnid] = $columnclass::from_column_name($this, $columnname);
} }
< $plugins = \core_component::get_plugin_list_with_class('qbank', 'plugin_feature', 'plugin_feature.php'); < foreach ($plugins as $componentname => $plugin) { < $pluginentrypointobject = new $plugin(); < $plugincolumnobjects = $pluginentrypointobject->get_question_columns($this); < // Don't need the plugins without column objects. < if (empty($plugincolumnobjects)) { < unset($plugins[$componentname]); < continue; < }
> > foreach ($this->plugins as $plugin) { > $plugincolumnobjects = $plugin->get_question_columns($this);
foreach ($plugincolumnobjects as $columnobject) {
< $columnname = $columnobject->get_column_name();
> $columnid = $columnobject->get_column_id();
foreach ($corequestionbankcolumns as $corequestionbankcolumn) {
< if (!\core\plugininfo\qbank::is_plugin_enabled($componentname)) { < unset($questionbankclasscolumns[$columnname]); < continue; < }
// Check if it has custom preference selector to view/hide. if ($columnobject->has_preference()) { if (!$columnobject->get_preference()) { continue; } }
< if ($corequestionbankcolumn === $columnname) { < $questionbankclasscolumns[$columnname] = $columnobject;
> if ($corequestionbankcolumn === $columnid) { > $questionbankclasscolumns[$columnid] = $columnobject;
} else { // Any community plugin for column/action.
< $newpluginclasscolumns[$columnname] = $columnobject;
> $newpluginclasscolumns[$columnid] = $columnobject;
} } } } // New plugins added at the end of the array, will change in sorting feature. foreach ($newpluginclasscolumns as $key => $newpluginclasscolumn) { $questionbankclasscolumns[$key] = $newpluginclasscolumn; }
< // Check if qbank_columnsortorder is enabled. < if (array_key_exists('columnsortorder', core_plugin_manager::instance()->get_enabled_plugins('qbank'))) { < $columnorder = new column_manager(); < $questionbankclasscolumns = $columnorder->get_sorted_columns($questionbankclasscolumns); < }
> $questionbankclasscolumns = $this->columnmanager->get_sorted_columns($questionbankclasscolumns); > $questionbankclasscolumns = $this->columnmanager->set_columns_visibility($questionbankclasscolumns);
// Mitigate the error in case of any regression. foreach ($questionbankclasscolumns as $shortname => $questionbankclasscolumn) {
< if (!is_object($questionbankclasscolumn)) {
> if (!is_object($questionbankclasscolumn) || !$questionbankclasscolumn->isvisible) {
unset($questionbankclasscolumns[$shortname]); } } return $questionbankclasscolumns; } /** * Loads all the available columns. * * @return array */ protected function wanted_columns(): array { $this->requiredcolumns = []; $questionbankcolumns = $this->get_question_bank_plugins(); foreach ($questionbankcolumns as $classobject) {
< if (empty($classobject)) {
> if (empty($classobject) || !($classobject instanceof \core_question\local\bank\column_base)) {
continue; } $this->requiredcolumns[$classobject->get_column_name()] = $classobject; } return $this->requiredcolumns; } /** * Check a column object from its name and get the object for sort. * * @param string $columnname */ protected function get_column_type($columnname) { if (empty($this->requiredcolumns[$columnname])) { $this->requiredcolumns[$columnname] = new $columnname($this); } } /** * Specify the column heading * * @return string Column name for the heading */ protected function heading_column(): string { return 'qbank_viewquestionname\viewquestionname_column_helper'; } /** * 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 = ''): void {
< // 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. $this->visiblecolumns = []; $this->extrarows = []; foreach ($wanted as $column) { if ($column->is_extra_row()) { $this->extrarows[$column->get_column_name()] = $column; } else {
> // Only add columns which are visible. $this->visiblecolumns[$column->get_column_name()] = $column; > if ($column->isvisible) {
} }
> }
if (array_key_exists($heading, $this->requiredcolumns)) { $this->requiredcolumns[$heading]->set_as_heading(); } } /** * Checks if the column included in the output. * * @param string $colname a column internal name. * @return bool is this column included in the output? */ public function has_column($colname): bool { return isset($this->visiblecolumns[$colname]); } /** * Get the count of the columns. * * @return int The number of columns in the table. */ public function get_column_count(): int { return count($this->visiblecolumns); } /** * Get course id. * @return mixed */ public function get_courseid() { return $this->course->id; } /** * Initialise sorting. */ protected function init_sort(): void {
< $this->init_sort_from_params(); < if (empty($this->sort)) { < $this->sort = $this->default_sort();
> $this->sort = []; > $sorts = optional_param_array('sortdata', [], PARAM_INT); > if (empty($sorts)) { > $sorts = $this->get_pagevars('sortdata'); > } > if (empty($sorts)) { > $sorts = $this->default_sort();
}
> $sorts = array_slice($sorts, 0, self::MAX_SORTS); } > foreach ($sorts as $sortname => $sortorder) { > // Deal with subsorts. /** > [$colname] = $this->parse_subsort($sortname); * Deal with a sort name of the form columnname, or colname_subsort by > $this->get_column_type($colname); * breaking it up, validating the bits that are present, and returning them. > } * If there is no subsort, then $subsort is returned as ''. > $this->sort = $sorts;
* * @param string $sort the sort parameter to process. * @return array [$colname, $subsort]. */ protected function parse_subsort($sort): array { // Do the parsing. if (strpos($sort, '-') !== false) { list($colname, $subsort) = explode('-', $sort, 2); } else { $colname = $sort; $subsort = ''; }
> $colname = str_replace('__', '\\', $colname);
// Validate the column name. $this->get_column_type($colname); $column = $this->requiredcolumns[$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);
> $this->baseurl->remove_params('sortdata'); > throw new \moodle_exception('unknownsortcolumn', '', $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);
> throw new \moodle_exception('unknownsortcolumn', '', $this->baseurl->out(), $sort);
} } return [$colname, $subsort]; } /**
< * Initialise sort from parameters. < */ < protected function init_sort_from_params(): void { < $this->sort = []; < 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) = $this->parse_subsort($sort); < $this->get_column_type($colname); < $this->sort[$sort] = $order; < } < } < < /**
* Sort to parameters. * * @param array $sorts * @return array */ protected function sort_to_params($sorts): array { $params = [];
< $i = 0; < foreach ($sorts as $sort => $order) { < $i += 1; < if ($order < 0) { < $sort = '-' . $sort; < } < $params['qbs' . $i] = $sort;
> foreach ($sorts as $sortname => $sortorder) { > $params['sortdata[' . $sortname . ']'] = $sortorder;
} return $params; } /** * Default sort for question data. * @return int[] */ protected function default_sort(): array { $defaultsort = []; if (class_exists('\\qbank_viewquestiontype\\question_type_column')) {
< $sort = 'qbank_viewquestiontype\question_type_column';
> $defaultsort['qbank_viewquestiontype__question_type_column'] = SORT_ASC;
}
< $defaultsort[$sort] = 1;
if (class_exists('\\qbank_viewquestionname\\question_name_idnumber_tags_column')) {
< $sort = 'qbank_viewquestionname\question_name_idnumber_tags_column';
> $defaultsort['qbank_viewquestionname__question_name_idnumber_tags_column-name'] = SORT_ASC;
}
< $defaultsort[$sort . '-name'] = 1;
return $defaultsort; } /** * Gets the primary sort order according to the default sort. *
< * @param string $sort a column or column_subsort name.
> * @param string $sortname 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): int {
> public function get_primary_sort_order($sortname): int {
$order = reset($this->sort); $primarysort = key($this->sort);
< if ($sort == $primarysort) {
> if ($sortname == $primarysort) {
return $order;
< } else { < return 0;
}
> } > 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 string $sortname 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): string { < if ($newsortreverse) { < $order = -1; < } else { < $order = 1; < }
> public function new_sort_url($sortname, $newsortreverse): string {
// 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]);
> if (isset($newsort[$sortname])) { > unset($newsort[$sortname]);
}
< $newsort[$sort] = $order;
> $newsort[$sortname] = $newsortreverse ? SORT_DESC : SORT_ASC;
$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, based on < * \core_question\bank\search\condition filters.
> * Return an array 'table_alias' => 'JOIN clause' to bring in any data that > * the core view requires. > * > * @return string[] 'table_alias' => 'JOIN clause'
*/
< protected function build_query(): void { < // Get the required tables and fields. < $joins = []; < $fields = ['qv.status', 'qc.id as categoryid', 'qv.version', 'qv.id as versionid', 'qbe.id as questionbankentryid']; < if (!empty($this->requiredcolumns)) { < foreach ($this->requiredcolumns as $column) { < $extrajoins = $column->get_extra_joins();
> protected function get_required_joins(): array { > return [ > 'qv' => 'JOIN {question_versions} qv ON qv.questionid = q.id', > 'qbe' => 'JOIN {question_bank_entries} qbe on qbe.id = qv.questionbankentryid', > 'qc' => 'JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid', > ]; > } > > /** > * Return an array of fields for any data that the core view requires. > * > * Use table alias 'q' for the question table, or one of the ones from get_required_joins. > * Every field requested must specify a table prefix. > * > * @return string[] fields required. > */ > protected function get_required_fields(): array { > return [ > 'q.id', > 'q.qtype', > 'q.createdby', > 'qc.id as categoryid', > 'qc.contextid', > 'qv.status', > 'qv.version', > 'qv.id as versionid', > 'qbe.id as questionbankentryid', > ]; > } > > /** > * Gather query requirements from view component objects. > * > * This will take the required fields and joins for this view, and combine them with those for all active view components. > * Fields will be de-duplicated in multiple components require the same field. > * Joins will be de-duplicated if the alias and join clause match exactly. > * > * @throws \coding_exception If two components attempt to use the same alias for different joins. > * @param view_component[] $viewcomponents List of component objects included in the current view > * @return array [$fields, $joins] SQL fields and joins to add to the query. > */ > protected function get_component_requirements(array $viewcomponents): array { > $fields = $this->get_required_fields(); > $joins = $this->get_required_joins(); > if (!empty($viewcomponents)) { > foreach ($viewcomponents as $viewcomponent) { > $extrajoins = $viewcomponent->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_merge($fields, $viewcomponent->get_required_fields()); > }
}
> return [array_unique($fields), $joins];
}
< $fields = array_unique($fields);
> > /** > * Create the SQL query to retrieve the indicated questions, based on > * \core_question\bank\search\condition filters. > */ > protected function build_query(): void { > // Get the required tables and fields. > [$fields, $joins] = $this->get_component_requirements(array_merge($this->requiredcolumns, $this->questionactions));
// Build the order by clause. $sorts = [];
< foreach ($this->sort as $sort => $order) { < list($colname, $subsort) = $this->parse_subsort($sort); < $sorts[] = $this->requiredcolumns[$colname]->sort_expression($order < 0, $subsort);
> foreach ($this->sort as $sortname => $sortorder) { > [$colname, $subsort] = $this->parse_subsort($sortname); > $sorts[] = $this->requiredcolumns[$colname]->sort_expression($sortorder == SORT_DESC, $subsort);
} // Build the where clause. $latestversion = 'qv.version = (SELECT MAX(v.version) FROM {question_versions} v JOIN {question_bank_entries} be ON be.id = v.questionbankentryid WHERE be.id = qbe.id)';
< $tests = ['q.parent = 0', $latestversion];
$this->sqlparams = [];
> $conditions = [];
foreach ($this->searchconditions as $searchcondition) { if ($searchcondition->where()) {
< $tests[] = '((' . $searchcondition->where() .'))';
> $conditions[] = '((' . $searchcondition->where() .'))';
} if ($searchcondition->params()) { $this->sqlparams = array_merge($this->sqlparams, $searchcondition->params()); } }
> // Get higher level filter condition. // Build the SQL. > $jointype = isset($this->pagevars['jointype']) ? (int)$this->pagevars['jointype'] : condition::JOINTYPE_DEFAULT; $sql = ' FROM {question} q ' . implode(' ', $joins); > $nonecondition = ($jointype === datafilter::JOINTYPE_NONE) ? ' NOT ' : ''; $sql .= ' WHERE ' . implode(' AND ', $tests); > $separator = ($jointype === datafilter::JOINTYPE_ALL) ? ' AND ' : ' OR ';
< $sql .= ' WHERE ' . implode(' AND ', $tests);
> $sql .= ' WHERE q.parent = 0 AND ' . $latestversion; > if (!empty($conditions)) { > $sql .= ' AND ' . $nonecondition . ' ( '; > $sql .= implode($separator, $conditions); > $sql .= ' ) '; > }
$this->loadsql = 'SELECT ' . implode(', ', $fields) . $sql . ' ORDER BY ' . implode(', ', $sorts); } /** * Get the number of questions.
> *
* @return int */
< protected function get_question_count(): int {
> public function get_question_count(): int {
global $DB;
< return $DB->count_records_sql($this->countsql, $this->sqlparams);
> if (is_null($this->totalcount)) { > $this->totalcount = $DB->count_records_sql($this->countsql, $this->sqlparams); > } > return $this->totalcount;
} /** * 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. */
< protected function load_page_questions($page, $perpage): \moodle_recordset {
> protected function load_page_questions(): \moodle_recordset {
global $DB;
< $questions = $DB->get_recordset_sql($this->loadsql, $this->sqlparams, $page * $perpage, $perpage);
> $questions = $DB->get_recordset_sql($this->loadsql, $this->sqlparams, > (int)$this->pagevars['qpage'] * (int)$this->pagevars['qperpage'], $this->pagevars['qperpage']);
if (empty($questions)) { $questions->close(); // No questions on this page. Reset to page 0.
< $questions = $DB->get_recordset_sql($this->loadsql, $this->sqlparams, 0, $perpage);
> $questions = $DB->get_recordset_sql($this->loadsql, $this->sqlparams, 0, $this->pagevars['qperpage']);
} return $questions; } /** * Returns the base url.
> * */ > * @return \moodle_url
public function base_url(): \moodle_url { return $this->baseurl; } /** * Get the URL for editing a question as a 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. */ public function edit_question_url($questionid) { return $this->edit_question_moodle_url($questionid)->out(); } /** * Get the URL for duplicating a question as a 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]); } /** * Get the URL for duplicating a given question. * @param int $questionid the question id. * @return string the URL, HTML-escaped. */ public function copy_question_url($questionid) { return $this->copy_question_moodle_url($questionid)->out(); } /** * Get the context we are displaying the question bank for. * @return \context context object. */ public function get_most_specific_context(): \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. * @deprecated since Moodle 4.0 * @see \qbank_previewquestion\helper::question_preview_url() * @todo Final deprecation on Moodle 4.4 MDL-72438 */ public function preview_question_url($questiondata) {
< debugging('Function preview_question_url() has been deprecated and moved to qbank_previewquestion plugin, < please use qbank_previewquestion\helper::question_preview_url() instead.', DEBUG_DEVELOPER);
> debugging( > 'Function preview_question_url() has been deprecated and moved to qbank_previewquestion plugin, ' . > 'please use qbank_previewquestion\helper::question_preview_url() instead.', > DEBUG_DEVELOPER > );
return question_preview_url($questiondata->id, null, null, null, null, $this->get_most_specific_context()); } /**
< * Shows the question bank interface.
> * Get fields from the pagevars array.
*
< * The function also processes a number of actions:
> * If a field is specified, that particlar pagevars field will be returned. Otherwise the entire array will be returned.
*
< * 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 < * params: $tabname question bank edit tab name, for permission checking < * $pagevars current list of page variables
> * If a field is specified but it does not exist, null will be returned.
*
< * @param string $tabname < * @param array $pagevars
> * @param ?string $field > * @return mixed
*/
< public function display($pagevars, $tabname): void {
> public function get_pagevars(?string $field = null): mixed { > if (is_null($field)) { > return $this->pagevars; > } else { > return $this->pagevars[$field] ?? null; > } > }
< $page = $pagevars['qpage']; < $perpage = $pagevars['qperpage']; < $cat = $pagevars['cat']; < $recurse = $pagevars['recurse']; < $showhidden = $pagevars['showhidden']; < $showquestiontext = $pagevars['qbshowtext']; < $tagids = []; < if (!empty($pagevars['qtagids'])) { < $tagids = $pagevars['qtagids'];
> /** > * Set the pagevars property with the provided array. > * > * @param array $pagevars > */ > public function set_pagevars(array $pagevars): void { > $this->pagevars = $pagevars;
}
< echo \html_writer::start_div('questionbankwindow boxwidthwide boxaligncenter');
> /** > * Shows the question bank interface. > */ > public function display(): void { > $editcontexts = $this->contexts->having_one_edit_tab_cap('questions');
< $editcontexts = $this->contexts->having_one_edit_tab_cap($tabname);
> echo \html_writer::start_div('questionbankwindow boxwidthwide boxaligncenter', [ > 'data-component' => 'core_question', > 'data-callback' => 'display_question_bank', > 'data-contextid' => $editcontexts[array_key_last($editcontexts)]->id, > ]);
// Show the filters and search options.
< $this->wanted_filters($cat, $tagids, $showhidden, $recurse, $editcontexts, $showquestiontext); <
> $this->wanted_filters();
// Continues with list of questions.
< $this->display_question_list($this->baseurl, $cat, null, $page, $perpage, < $this->contexts->having_cap('moodle/question:add'));
> $this->display_question_list();
echo \html_writer::end_div(); } /** * The filters for the question bank.
< * < * @param string $cat 'categoryid,contextid' < * @param array $tagids current list of selected tags < * @param bool $showhidden whether deleted questions should be displayed < * @param int $recurse Whether to include subcategories < * @param array $editcontexts parent contexts < * @param bool $showquestiontext whether the text of each question should be shown in the list < */ < public function wanted_filters($cat, $tagids, $showhidden, $recurse, $editcontexts, $showquestiontext): void { < global $CFG; < list(, $contextid) = explode(',', $cat);
> */ > public function wanted_filters(): void { > global $OUTPUT; > [, $contextid] = explode(',', $this->pagevars['cat']);
$catcontext = \context::instance_by_id($contextid);
< $thiscontext = $this->get_most_specific_context();
// Category selection form. $this->display_question_bank_header();
< < // Display tag filter if usetags setting is enabled/enablefilters is true. < if ($this->enablefilters) { < if (is_array($this->customfilterobjects)) { < foreach ($this->customfilterobjects as $filterobjects) { < $this->searchconditions[] = $filterobjects; < } < } else { < if ($CFG->usetags) { < array_unshift($this->searchconditions, < new \core_question\bank\search\tag_condition([$catcontext, $thiscontext], $tagids)); < } < < 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);
> // Add search conditions. > $this->add_standard_search_conditions(); > // Render the question bank filters. > $additionalparams = [ > 'perpage' => $this->pagevars['qperpage'], > ]; > $filter = new question_bank_filter_ui($catcontext, $this->searchconditions, $additionalparams, $this->component, > $this->callback, static::class, 'qbank-table', $this->cm?->id, $this->pagevars, > $this->extraparams); > echo $OUTPUT->render($filter);
} /** * Print the text if category id not available.
> * */ > * @deprecated since Moodle 4.3 MDL-72321 protected function print_choose_category_message(): void { > * @todo Final deprecation on Moodle 4.7 MDL-78090
echo \html_writer::start_tag('p', ['style' => "\"text-align:center;\""]);
> debugging( echo \html_writer::tag('b', get_string('selectcategoryabove', 'question')); > 'Function print_choose_category_message() is deprecated, all the features for this method is currently ' . echo \html_writer::end_tag('p'); > 'handled by the qbank filter api, please have a look at ' . } > 'question/bank/managecategories/classes/category_confition.php for more information.', > DEBUG_DEVELOPER /** > );
* Gets current selected category. * @param string $categoryandcontext * @return false|mixed|\stdClass
> * */ > * @deprecated since Moodle 4.3 MDL-72321 protected function get_current_category($categoryandcontext) { > * @todo Final deprecation on Moodle 4.7 MDL-78090
global $DB, $OUTPUT;
> debugging( list($categoryid, $contextid) = explode(',', $categoryandcontext); > 'Function get_current_category() is deprecated, all the features for this method is currently handled by ' . if (!$categoryid) { > 'the qbank filter api, please have a look at question/bank/managecategories/classes/category_confition.php ' . $this->print_choose_category_message(); > 'for more information.', return false; > DEBUG_DEVELOPER } > );
if (!$category = $DB->get_record('question_categories', ['id' => $categoryid, 'contextid' => $contextid])) { echo $OUTPUT->box_start('generalbox questionbank'); echo $OUTPUT->notification('Category not found!'); echo $OUTPUT->box_end(); return false; } return $category; } /** * 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.
> * @deprecated since Moodle 4.3 MDL-72321 */ > * @todo Final deprecation on Moodle 4.7 MDL-78090
protected function display_options_form($showquestiontext): void {
> debugging( global $PAGE; > 'Function display_options_form() is deprecated, this method has been replaced with mustaches in filters, ' . > 'please use filtering objects', // The html will be refactored in the filter feature implementation. > DEBUG_DEVELOPER echo \html_writer::start_tag('form', ['method' => 'get', > );
'action' => new \moodle_url($this->baseurl), 'id' => 'displayoptions']); echo \html_writer::start_div(); $excludes = ['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); $advancedsearch = []; foreach ($this->searchconditions as $searchcondition) { if ($searchcondition->display_options_adv()) { $advancedsearch[] = $searchcondition; }
< echo $searchcondition->display_options();
}
< $this->display_showtext_checkbox($showquestiontext);
if (!empty($advancedsearch)) { $this->display_advanced_search_form($advancedsearch); } $go = \html_writer::empty_tag('input', ['type' => 'submit', 'value' => get_string('go')]); echo \html_writer::tag('noscript', \html_writer::div($go), ['class' => 'inline']); echo \html_writer::end_div(); echo \html_writer::end_tag('form'); $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. * * @param array $advancedsearch
> * @deprecated since Moodle 4.3 MDL-72321 */ > * @todo Final deprecation on Moodle 4.7 MDL-78090
protected function display_advanced_search_form($advancedsearch): void {
> debugging( print_collapsible_region_start('', 'advancedsearch', > 'Function display_advanced_search_form() is deprecated, this method has been replaced with mustaches in ' . get_string('advancedsearchoptions', 'question'), > 'filters, please use filtering objects', 'question_bank_advanced_search'); > DEBUG_DEVELOPER foreach ($advancedsearch as $searchcondition) { > );
echo $searchcondition->display_options_adv(); } 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.
> * @deprecated since Moodle 4.3 MDL-72321 */ > * @todo Final deprecation on Moodle 4.7 MDL-78090
protected function display_showtext_checkbox($showquestiontext): void {
> debugging('Function display_showtext_checkbox() is deprecated, please use filtering objects', DEBUG_DEVELOPER);
global $PAGE; $displaydata = [ 'checked' => $showquestiontext ]; if (class_exists('qbank_viewquestiontext\\question_text_row')) { if (\core\plugininfo\qbank::is_plugin_enabled('qbank_viewquestiontext')) { echo $PAGE->get_renderer('core_question', 'bank')->render_showtext_checkbox($displaydata); } } } /** * Display the header element for the question bank. */ protected function display_question_bank_header(): void { global $OUTPUT; echo $OUTPUT->heading(get_string('questionbank', 'question'), 2); } /**
< * Create a new question form.
> * Does the current view allow adding new questions?
*
< * @param false|mixed|\stdClass $category < * @param bool $canadd
> * @return bool True if the view supports adding new questions.
*/
< protected function create_new_question_form($category, $canadd): void { < if (\core\plugininfo\qbank::is_plugin_enabled('qbank_editquestion')) { < echo editquestion_helper::create_new_question_button($category->id, < $this->requiredcolumns['edit_action_column']->editquestionurl->params(), $canadd); < }
> public function allow_add_questions(): bool { > return true;
} /**
< * Prints the table of questions in a category with interactions
> * Output the question bank controls for each plugin.
*
< * @param \moodle_url $pageurl The URL to reload this page. < * @param string $categoryandcontext 'categoryID,contextID'. < * @param int $recurse Whether to include subcategories. < * @param int $page The number of the page to be displayed < * @param int|null $perpage Number of questions to show per page < * @param array $addcontexts contexts where the user is allowed to add new questions.
> * Controls will be output in the order defined by the array keys returned from > * {@see plugin_features_base::get_question_bank_controls}. If more than one plugin defines a control in the same position, > * they will placed after one another based on the alphabetical order of the plugins. > * > * @param \core\context $context The current context, for permissions checks. > * @param int $categoryid The current question category.
*/
< protected function display_question_list($pageurl, $categoryandcontext, $recurse = 1, $page = 0, < $perpage = null, $addcontexts = []): void {
> protected function get_plugin_controls(\core\context $context, int $categoryid): string {
global $OUTPUT;
> $orderedcontrols = []; // This function can be moderately slow with large question counts and may time out. > foreach ($this->plugins as $plugin) { // We probably do not want to raise it to unlimited, so randomly picking 5 minutes. > $plugincontrols = $plugin->get_question_bank_controls($this, $context, $categoryid); // Note: We do not call this in the loop because quiz ob_ captures this function (see raise() PHP doc). > foreach ($plugincontrols as $position => $plugincontrol) { \core_php_time_limit::raise(300); > if (!array_key_exists($position, $orderedcontrols)) { > $orderedcontrols[$position] = []; $category = $this->get_current_category($categoryandcontext); > } $perpage = $perpage ?? $this->pagesize; > $orderedcontrols[$position][] = $plugincontrol; > } list($categoryid, $contextid) = explode(',', $categoryandcontext); > } $catcontext = \context::instance_by_id($contextid); > ksort($orderedcontrols); > $output = ''; $canadd = has_capability('moodle/question:add', $catcontext); > foreach ($orderedcontrols as $controls) { > foreach ($controls as $control) { $this->create_new_question_form($category, $canadd); > $output .= $OUTPUT->render($control); > } $this->build_query(); > } $totalnumber = $this->get_question_count(); > return $OUTPUT->render_from_template('core_question/question_bank_controls', ['controls' => $output]); if ($totalnumber == 0) { > } return; > } > /** $questionsrs = $this->load_page_questions($page, $perpage); > * Prints the table of questions in a category with interactions $questions = []; > */ foreach ($questionsrs as $question) { > public function display_question_list(): void {
< $category = $this->get_current_category($categoryandcontext); < $perpage = $perpage ?? $this->pagesize; < < list($categoryid, $contextid) = explode(',', $categoryandcontext);
> [$categoryid, $contextid] = category_condition::validate_category_param($this->pagevars['cat']);
< $canadd = has_capability('moodle/question:add', $catcontext); < < $this->create_new_question_form($category, $canadd);
> echo \html_writer::start_tag( > 'div', > [ > 'id' => 'questionscontainer', > 'data-component' => $this->component, > 'data-callback' => $this->callback, > 'data-contextid' => $this->get_most_specific_context()->id, > ] > ); > echo $this->get_plugin_controls($catcontext, $categoryid);
< $totalnumber = $this->get_question_count(); < if ($totalnumber == 0) { < return; < } < $questionsrs = $this->load_page_questions($page, $perpage);
> $questionsrs = $this->load_page_questions(); > $totalquestions = $this->get_question_count();
< // Bulk load any required statistics. < $this->load_required_statistics($questions); < < // Bulk load any extra data that any column requires. < foreach ($this->requiredcolumns as $name => $column) { < $column->load_additional_data($questions); < } < < $pageingurl = new \moodle_url($pageurl, $pageurl->params()); < $pagingbar = new \paging_bar($totalnumber, $page, $perpage, $pageingurl); < $pagingbar->pagevar = 'qpage'; < < $this->display_top_pagnation($OUTPUT->render($pagingbar)); <
< echo \html_writer::start_tag('form', ['action' => $pageurl, 'method' => 'post', 'id' => 'questionsubmit']);
> echo \html_writer::start_tag('form', ['action' => $this->baseurl, 'method' => 'post', 'id' => 'questionsubmit']);
< $this->display_questions($questions);
> $filtercondition = json_encode($this->get_pagevars()); > // Embeded filterconditon into the div. > echo \html_writer::start_tag('div', > ['class' => 'categoryquestionscontainer', 'data-filtercondition' => $filtercondition]); > if ($totalquestions > 0) { > // Bulk load any required statistics. > $this->load_required_statistics($questions);
< $this->display_bottom_pagination($OUTPUT->render($pagingbar), $totalnumber, $perpage, $pageurl);
> // Bulk load any extra data that any column requires. > foreach ($this->requiredcolumns as $column) { > $column->load_additional_data($questions); > } > $this->display_questions($questions, $this->pagevars['qpage'], $this->pagevars['qperpage']); > } > echo \html_writer::end_tag('div');
$this->display_bottom_controls($catcontext); echo \html_writer::end_tag('fieldset'); echo \html_writer::end_tag('form');
> echo \html_writer::end_tag('div');
} /** * Work out the list of all the required statistics fields for this question bank view. * * This gathers all the required fields from all columns, so they can all be loaded at once. * * @return string[] the names of all the required fields for this question bank view. */ protected function determine_required_statistics(): array { $requiredfields = []; foreach ($this->requiredcolumns as $column) { $requiredfields = array_merge($requiredfields, $column->get_required_statistics_fields()); } return array_unique($requiredfields); } /** * Load the aggregate statistics that all the columns require. * * @param \stdClass[] $questions the questions that will be displayed indexed by question id. */ protected function load_required_statistics(array $questions): void { $requiredstatistics = $this->determine_required_statistics(); $this->loadedstatistics = statistics_bulk_loader::load_aggregate_statistics( array_keys($questions), $requiredstatistics); } /** * Get the aggregated value of a particular statistic for a particular question. * * You can only get values for the questions on the current page of the question bank view, * and only if you declared the need for this statistic in the get_required_statistics_fields() * method of your question bank column. * * @param int $questionid the id of a question * @param string $fieldname the name of a statistics field, e.g. 'facility'. * @return float|null the average (across all users) of this statistic for this question. * Null if the value is not available right now. */ public function get_aggregate_statistic(int $questionid, string $fieldname): ?float { if (!array_key_exists($questionid, $this->loadedstatistics)) { throw new \coding_exception('Question ' . $questionid . ' is not on the current page of ' . 'this question bank view, so its statistics are not available.'); } // Must be array_key_exists, not isset, because we care about null values. if (!array_key_exists($fieldname, $this->loadedstatistics[$questionid])) { throw new \coding_exception('Statistics field ' . $fieldname . ' was not requested by any ' . 'question bank column in this view, so it is not available.'); } return $this->loadedstatistics[$questionid][$fieldname]; } /** * Display the top pagination bar. * * @param object $pagination
> * @deprecated since Moodle 4.3 */ > * @todo Final deprecation on Moodle 4.7 MDL-78091
< protected function display_top_pagnation($pagination): void {
> public function display_top_pagnation($pagination): void { > debugging( > 'Function display_top_pagnation() is deprecated, please use display_questions() for ajax based pagination.', > DEBUG_DEVELOPER > );
global $PAGE; $displaydata = [ 'pagination' => $pagination ]; echo $PAGE->get_renderer('core_question', 'bank')->render_question_pagination($displaydata); } /** * Display bottom pagination bar. * * @param string $pagination * @param int $totalnumber * @param int $perpage * @param \moodle_url $pageurl
> * @deprecated since Moodle 4.3 */ > * @todo Final deprecation on Moodle 4.7 MDL-78091
< protected function display_bottom_pagination($pagination, $totalnumber, $perpage, $pageurl): void {
> public function display_bottom_pagination($pagination, $totalnumber, $perpage, $pageurl): void { > debugging( > 'Function display_bottom_pagination() is deprecated, please use display_questions() for ajax based pagination.', > DEBUG_DEVELOPER > );
global $PAGE; $displaydata = array ( 'extraclasses' => 'pagingbottom', 'pagination' => $pagination, 'biggertotal' => true, ); if ($totalnumber > $this->pagesize) { $displaydata['showall'] = true; if ($perpage == $this->pagesize) { $url = new \moodle_url($pageurl, array_merge($pageurl->params(), ['qpage' => 0, 'qperpage' => MAXIMUM_QUESTIONS_PER_PAGE])); if ($totalnumber > MAXIMUM_QUESTIONS_PER_PAGE) { $displaydata['totalnumber'] = MAXIMUM_QUESTIONS_PER_PAGE; } else { $displaydata['biggertotal'] = false; $displaydata['totalnumber'] = $totalnumber; } } else { $url = new \moodle_url($pageurl, array_merge($pageurl->params(), ['qperpage' => $this->pagesize])); $displaydata['totalnumber'] = $this->pagesize; } $displaydata['showallurl'] = $url; } echo $PAGE->get_renderer('core_question', 'bank')->render_question_pagination($displaydata); } /** * Display the controls at the bottom of the list of questions. * * @param \context $catcontext The context of the category being displayed. */ protected function display_bottom_controls(\context $catcontext): void { $caneditall = has_capability('moodle/question:editall', $catcontext); $canuseall = has_capability('moodle/question:useall', $catcontext); $canmoveall = has_capability('moodle/question:moveall', $catcontext); if ($caneditall || $canmoveall || $canuseall) { global $PAGE; $bulkactiondatas = []; $params = $this->base_url()->params();
< $params['returnurl'] = $this->base_url();
> $returnurl = new \moodle_url($this->base_url(), ['filter' => json_encode($this->pagevars['filter'])]); > $params['returnurl'] = $returnurl;
foreach ($this->bulkactions as $key => $action) { // Check capabilities. $capcount = 0; foreach ($action['capabilities'] as $capability) { if (has_capability($capability, $catcontext)) { $capcount ++; } } // At least one cap need to be there. if ($capcount === 0) { unset($this->bulkactions[$key]); continue; } $actiondata = new \stdClass(); $actiondata->actionname = $action['title']; $actiondata->actionkey = $key; $actiondata->actionurl = new \moodle_url($action['url'], $params); $bulkactiondata[] = $actiondata; $bulkactiondatas ['bulkactionitems'] = $bulkactiondata; } // We dont need to show this section if none of the plugins are enabled. if (!empty($bulkactiondatas)) { echo $PAGE->get_renderer('core_question', 'bank')->render_bulk_actions_ui($bulkactiondatas); } } } /** * Display the questions. * * @param array $questions */
< protected function display_questions($questions): void {
> public function display_questions($questions, $page = 0, $perpage = DEFAULT_QUESTIONS_PER_PAGE): void { > global $OUTPUT; > if (!isset($this->pagevars['filter']['category'])) { > // We must have a category filter selected. > echo $OUTPUT->render_from_template('qbank_managecategories/choose_category', []); > return; > } > // Pagination. > $pageingurl = new \moodle_url($this->base_url()); > $pagingbar = new \paging_bar($this->totalcount, $page, $perpage, $pageingurl); > $pagingbar->pagevar = 'qpage'; > echo $OUTPUT->render($pagingbar); > > // Table of questions.
echo \html_writer::start_tag('div',
< ['class' => 'categoryquestionscontainer', 'id' => 'questionscontainer']);
> ['class' => 'question_table', 'id' => 'question_table']);
$this->print_table($questions); echo \html_writer::end_tag('div');
> echo $OUTPUT->render($pagingbar); } > } > /** > /** * Prints the actual table with question. > * Load the questions according to the search conditions. * > * * @param array $questions > * @return array */ > */ protected function print_table($questions): void { > public function load_questions() { // Start of the table. > $this->build_query(); echo \html_writer::start_tag('table', ['id' => 'categoryquestions', 'class' => 'table-responsive']); > $questionsrs = $this->load_page_questions(); > $questions = []; // Prints the table header. > foreach ($questionsrs as $question) { echo \html_writer::start_tag('thead'); > if (!empty($question->id)) { echo \html_writer::start_tag('tr'); > $questions[$question->id] = $question; $this->print_table_headers(); > } echo \html_writer::end_tag('tr'); > } echo \html_writer::end_tag('thead'); > $questionsrs->close(); > foreach ($this->requiredcolumns as $name => $column) { // Prints the table row or content. > $column->load_additional_data($questions); echo \html_writer::start_tag('tbody'); > } $rowcount = 0; > return $questions;
< echo \html_writer::start_tag('table', ['id' => 'categoryquestions', 'class' => 'table-responsive']);
> echo \html_writer::start_tag('table', [ > 'id' => 'categoryquestions', > 'class' => 'question-bank-table generaltable', > 'data-defaultsort' => json_encode($this->sort), > ]);
< echo \html_writer::start_tag('tr');
> echo \html_writer::start_tag('tr', ['class' => 'qbank-column-list']);
$rowcount += 1; } echo \html_writer::end_tag('tbody'); // End of the table. echo \html_writer::end_tag('table'); } /** * Start of the table html. *
< * @deprecated since Moodle 4.0
* @see print_table()
< * @todo Final deprecation on Moodle 4.4 MDL-72438
> * @deprecated since Moodle 4.3 MDL-72321 > * @todo Final deprecation on Moodle 4.7 MDL-78090
*/ protected function start_table() { debugging('Function start_table() is deprecated, please use print_table() instead.', DEBUG_DEVELOPER); echo '<table id="categoryquestions" class="table table-responsive">' . "\n"; echo "<thead>\n"; $this->print_table_headers(); echo "</thead>\n"; echo "<tbody>\n"; } /** * End of the table html. *
< * @deprecated since Moodle 4.0
* @see print_table()
< * @todo Final deprecation on Moodle 4.4 MDL-72438
> * @deprecated since Moodle 4.3 MDL-72321 > * @todo Final deprecation on Moodle 4.7 MDL-78090
*/ protected function end_table() { debugging('Function end_table() is deprecated, please use print_table() instead.', DEBUG_DEVELOPER); echo "</tbody>\n"; echo "</table>\n"; } /** * Print table headers from child classes. */ protected function print_table_headers(): void {
> $columnactions = $this->columnmanager->get_column_actions($this);
foreach ($this->visiblecolumns as $column) {
< $column->display_header();
> $width = $this->columnmanager->get_column_width($column); > $column->display_header($columnactions, $width);
} } /** * Gets the classes for the row. * * @param \stdClass $question * @param int $rowcount * @return array */ protected function get_row_classes($question, $rowcount): array { $classes = []; if ($question->status === question_version_status::QUESTION_STATUS_HIDDEN) { $classes[] = 'dimmed_text'; } if ($question->id == $this->lastchangedid) { $classes[] = 'highlight text-dark'; } $classes[] = 'r' . ($rowcount % 2); return $classes; } /** * Prints the table row from child classes. * * @param \stdClass $question * @param int $rowcount */
< protected function print_table_row($question, $rowcount): void {
> public function print_table_row($question, $rowcount): void {
$rowclasses = implode(' ', $this->get_row_classes($question, $rowcount)); $attributes = []; if ($rowclasses) { $attributes['class'] = $rowclasses; } echo \html_writer::start_tag('tr', $attributes); foreach ($this->visiblecolumns as $column) { $column->display($question, $rowclasses); } echo \html_writer::end_tag('tr'); foreach ($this->extrarows as $row) { $row->display($question, $rowclasses); } } /** * Process actions for the selected action. * @deprecated since Moodle 4.0 * @todo Final deprecation on Moodle 4.4 MDL-72438 */ public function process_actions(): void { debugging('Function process_actions() is deprecated and its code has been completely deleted. Please, remove the call from your code and check core_question\local\bank\bulk_action_base to learn more about bulk actions in qbank.', DEBUG_DEVELOPER); // Associated code is deleted to make sure any incorrect call doesnt not cause any data loss. } /** * Process actions with ui. * @return bool * @deprecated since Moodle 4.0 * @todo Final deprecation on Moodle 4.4 MDL-72438 */ public function process_actions_needing_ui(): bool { debugging('Function process_actions_needing_ui() is deprecated and its code has been completely deleted. Please, remove the call from your code and check core_question\local\bank\bulk_action_base to learn more about bulk actions in qbank.', DEBUG_DEVELOPER); // Associated code is deleted to make sure any incorrect call doesnt not cause any data loss. return false; } /** * Add another search control to this view. * @param condition $searchcondition the condition to add.
> * @param string|null $fieldname
*/
< public function add_searchcondition($searchcondition): void {
> public function add_searchcondition(condition $searchcondition, ?string $fieldname = null): void { > if (is_null($fieldname)) {
$this->searchconditions[] = $searchcondition;
> } else { } > $this->searchconditions[$fieldname] = $searchcondition; > } /** > } * Gets visible columns. > * @return array Visible columns. > /** */ > * Add standard search conditions. public function get_visiblecolumns(): array { > * Params must be set into this object before calling this function. return $this->visiblecolumns; > */ } > public function add_standard_search_conditions(): void { > foreach ($this->plugins as $componentname => $plugin) { /** > if (\core\plugininfo\qbank::is_plugin_enabled($componentname)) { * Is this view showing separate versions of a question? > $pluginentrypointobject = new $plugin(); * > $pluginobjects = $pluginentrypointobject->get_question_filters($this); * @return bool > foreach ($pluginobjects as $pluginobject) { */ > $this->add_searchcondition($pluginobject, $pluginobject->get_condition_key()); public function is_listing_specific_versions(): bool { > } return false; > } } > }
}
> } > > /** > * Return array of menu actions. > * > * @return question_action_base[] > */ > public function get_question_actions(): array { > return $this->questionactions; > } > > /** > * Display the questions table for the fragment/ajax. > * > * @return string HTML for the question table > */ > public function display_questions_table(): string { > $this->add_standard_search_conditions(); > $questions = $this->load_questions(); > $totalquestions = $this->get_question_count(); > $questionhtml = ''; > if ($totalquestions > 0) { > $this->load_required_statistics($questions); > ob_start(); > $this->display_questions($questions, $this->pagevars['qpage'], $this->pagevars['qperpage']); > $questionhtml = ob_get_clean(); > } > return $questionhtml;