Rev 1 | Ir a la última revisión | Autoría | Comparar con el anterior | Ultima modificación | Ver Log |
<?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;defined('MOODLE_INTERNAL') || die();require_once($CFG->dirroot . '/question/editlib.php');use coding_exception;use core\plugininfo\qbank;use core\output\datafilter;use core_plugin_manager;use core_question\local\bank\condition;use core_question\local\statistics\statistics_bulk_loader;use core_question\output\question_bank_filter_ui;use core_question\local\bank\column_manager_base;use qbank_deletequestion\hidden_condition;use qbank_editquestion\editquestion_helper;use qbank_managecategories\category_condition;/*** This class prints a view of the question bank.** including* + Some controls to allow users to to select what is displayed.* + A list of questions as a table.* + Further controls to do things with the questions.** This class gives a basic view, and provides plenty of hooks where subclasses* can override parts of the display.** The list of questions presented as a table is generated by creating a list of* core_question\bank\column objects, one for each 'column' to be displayed. These* manage* + outputting the contents of that column, given a $question object, but also* + generating the right fragments of SQL to ensure the necessary data is present,* and sorted in the right order.* + outputting table headers.** @copyright 2009 Tim Hunt* @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*/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.** Array keys are the class name.*/protected $questionactions;/*** @var column_base[] these are the 'columns' that are* actually displayed as a column, in order. Array keys are the class name.*/protected $visiblecolumns;/*** @var column_base[] these are the 'columns' that are* common to the question bank.*/protected $corequestionbankcolumns;/*** @var 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.*/protected $extrarows;/*** @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 array $bulkactions to identify the bulk actions for the api.*/public $bulkactions = [];/*** @var int|null Number of questions.*/protected $totalcount = null;/*** @var array Parameters for the page URL.*/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 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, $params = [], $extraparams = []) {$this->contexts = $contexts;$this->baseurl = $pageurl;$this->course = $course;$this->cm = $cm;$this->extraparams = $extraparams;// Default filter condition.if (!isset($params['filter']) && isset($params['cat'])) {$params['filter'] = [];[$categoryid, $contextid] = category_condition::validate_category_param($params['cat']);if (!is_null($categoryid)) {$category = category_condition::get_category_record($categoryid, $contextid);$params['filter']['category'] = ['jointype' => category_condition::JOINTYPE_DEFAULT,'values' => [$category->id],'filteroptions' => ['includesubcategories' => false],];}$params['filter']['hidden'] = ['jointype' => hidden_condition::JOINTYPE_DEFAULT,'values' => [0],];$params['jointype'] = datafilter::JOINTYPE_ALL;}if (!empty($params['filter'])) {$params['filter'] = filter_condition_manager::unpack_filteroptions_param($params['filter']);}if (isset($params['filter']['jointype'])) {$params['jointype'] = $params['filter']['jointype'];unset($params['filter']['jointype']);}// Create the url of the new question page to forward to.$this->returnurl = $pageurl->out_as_local_url(false);$this->editquestionurl = new \moodle_url('/question/bank/editquestion/question.php', ['returnurl' => $this->returnurl]);if ($this->cm !== null) {$this->editquestionurl->param('cmid', $this->cm->id);} else {$this->editquestionurl->param('courseid', $this->course->id);}$this->lastchangedid = clean_param($pageurl->param('lastchanged'), PARAM_INT);$this->init_plugins();$this->init_column_manager();// Possibly the heading part can be removed.$this->set_pagevars($params);$this->init_columns($this->wanted_columns(), $this->heading_column());$this->init_question_actions();$this->init_sort();$this->init_bulk_actions();}/*** Get an array of plugin features objects for all enabled qbank plugins.** @return void*/protected function init_plugins(): void {$plugins = \core_component::get_plugin_list_with_class('qbank', 'plugin_feature', 'plugin_feature.php');foreach ($plugins as $componentname => $pluginclass) {if (!\core\plugininfo\qbank::is_plugin_enabled($componentname)) {continue;}$this->plugins[$componentname] = new $pluginclass();}// Sort plugin list by component name.ksort($this->plugins);}/*** 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();if (!is_array($bulkactions)) {debugging("The method {$componentname}::get_bulk_actions() must return an " ."array of bulk actions instead of a single bulk action. " ."Please update your implementation of get_bulk_actions() to return an array. " ."Check out the qbank_bulkmove plugin for a working example.", DEBUG_DEVELOPER);$bulkactions = [$bulkactions];}foreach ($bulkactions as $bulkactionobject) {$this->bulkactions[$bulkactionobject->get_key()] = ['title' => $bulkactionobject->get_bulk_action_title(),'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* @todo Final deprecation on Moodle 4.7 MDL-78090*/protected function init_search_conditions(): void {debugging('Function init_search_conditions() has been deprecated, please create a qbank plugin' .'and implement a filter object instead.',DEBUG_DEVELOPER);$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);}}}/*** 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;if ($menuaction->get_menu_position() === question_action_base::MENU_POSITION_NOT_SET) {debugging('Question bank actions must define the get_menu_position method. ' .$menuaction::class . ' does not.', DEBUG_DEVELOPER);}}}// Sort according to each action's desired position.// Note, we are relying on the sort to be stable for// equal values of get_menu_position.uasort($this->questionactions,function (question_action_base $a, question_action_base $b) {return $a->get_menu_position() <=> $b->get_menu_position();},);}/*** Get class for each question bank columns.** @return array*/protected function get_question_bank_plugins(): array {$questionbankclasscolumns = [];$newpluginclasscolumns = [];$corequestionbankcolumns = ['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',];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);}}foreach ($this->plugins as $plugin) {$plugincolumnobjects = $plugin->get_question_columns($this);foreach ($plugincolumnobjects as $columnobject) {$columnid = $columnobject->get_column_id();foreach ($corequestionbankcolumns as $corequestionbankcolumn) {// Check if it has custom preference selector to view/hide.if ($columnobject->has_preference()) {if (!$columnobject->get_preference()) {continue;}}if ($corequestionbankcolumn === $columnid) {$questionbankclasscolumns[$columnid] = $columnobject;} else {// Any community plugin for column/action.$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;}$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) || !$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) || !($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 {// 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.if ($column->isvisible) {$this->visiblecolumns[$column->get_column_name()] = $column;}}}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->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);$this->get_column_type($colname);}$this->sort = $sorts;}/*** Deal with a sort name of the form columnname, or colname_subsort by* breaking it up, validating the bits that are present, and returning them.* If there is no subsort, then $subsort is returned as ''.** @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()) {$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', '', $this->baseurl->out(), $sort);}}return [$colname, $subsort];}/*** Sort to parameters.** @param array $sorts* @return array*/protected function sort_to_params($sorts): array {$params = [];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')) {$defaultsort['qbank_viewquestiontype__question_type_column'] = SORT_ASC;}if (class_exists('\\qbank_viewquestionname\\question_name_idnumber_tags_column')) {$defaultsort['qbank_viewquestionname__question_name_idnumber_tags_column-name'] = SORT_ASC;}return $defaultsort;}/*** Gets the primary sort order according to the default sort.** @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($sortname): int {$order = reset($this->sort);$primarysort = key($this->sort);if ($sortname == $primarysort) {return $order;}return 0;}/*** Get a URL to redisplay the page with a new sort for the question bank.** @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($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[$sortname])) {unset($newsort[$sortname]);}$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));}/*** 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 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, $viewcomponent->get_required_fields());}}return [array_unique($fields), $joins];}/*** 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 $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} vJOIN {question_bank_entries} beON be.id = v.questionbankentryidWHERE be.id = qbe.id)';$this->sqlparams = [];$conditions = [];foreach ($this->searchconditions as $searchcondition) {if ($searchcondition->where()) {$conditions[] = '((' . $searchcondition->where() .'))';}if ($searchcondition->params()) {$this->sqlparams = array_merge($this->sqlparams, $searchcondition->params());}}// Get higher level filter condition.$jointype = isset($this->pagevars['jointype']) ? (int)$this->pagevars['jointype'] : condition::JOINTYPE_DEFAULT;$nonecondition = ($jointype === datafilter::JOINTYPE_NONE) ? ' NOT ' : '';$separator = ($jointype === datafilter::JOINTYPE_ALL) ? ' AND ' : ' OR ';// Build the SQL.$sql = ' FROM {question} q ' . implode(' ', $joins);$sql .= ' WHERE q.parent = 0 AND ' . $latestversion;if (!empty($conditions)) {$sql .= ' AND ' . $nonecondition . ' ( ';$sql .= implode($separator, $conditions);$sql .= ' ) ';}$this->countsql = 'SELECT count(1)' . $sql;$this->loadsql = 'SELECT ' . implode(', ', $fields) . $sql . ' ORDER BY ' . implode(', ', $sorts);}/*** Get the number of questions.** @return int*/public function get_question_count(): int {global $DB;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.** @return \moodle_recordset questionid => data about each question.*/protected function load_page_questions(): \moodle_recordset {global $DB;$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, $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();}/*** @deprecated since Moodle 4.0*/public function preview_question_url() {throw new coding_exception(__FUNCTION__ . '() has been removed.');}/*** Get fields from the pagevars array.** If a field is specified, that particlar pagevars field will be returned. Otherwise the entire array will be returned.** If a field is specified but it does not exist, null will be returned.** @param ?string $field* @return mixed*/public function get_pagevars(?string $field = null): mixed {if (is_null($field)) {return $this->pagevars;} else {return $this->pagevars[$field] ?? null;}}/*** Set the pagevars property with the provided array.** @param array $pagevars*/public function set_pagevars(array $pagevars): void {$this->pagevars = $pagevars;}/*** Shows the question bank interface.*/public function display(): void {$editcontexts = $this->contexts->having_one_edit_tab_cap('questions');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();// Continues with list of questions.$this->display_question_list();echo \html_writer::end_div();}/*** The filters for the question bank.*/public function wanted_filters(): void {global $OUTPUT;[, $contextid] = explode(',', $this->pagevars['cat']);$catcontext = \context::instance_by_id($contextid);// Category selection form.$this->display_question_bank_header();// 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* @todo Final deprecation on Moodle 4.7 MDL-78090*/protected function print_choose_category_message(): void {debugging('Function print_choose_category_message() is deprecated, all the features for this method is currently ' .'handled by the qbank filter api, please have a look at ' .'question/bank/managecategories/classes/category_confition.php for more information.',DEBUG_DEVELOPER);echo \html_writer::start_tag('p', ['style' => "\"text-align:center;\""]);echo \html_writer::tag('b', get_string('selectcategoryabove', 'question'));echo \html_writer::end_tag('p');}/*** Gets current selected category.* @param string $categoryandcontext* @return false|mixed|\stdClass** @deprecated since Moodle 4.3 MDL-72321* @todo Final deprecation on Moodle 4.7 MDL-78090*/protected function get_current_category($categoryandcontext) {debugging('Function get_current_category() is deprecated, all the features for this method is currently handled by ' .'the qbank filter api, please have a look at question/bank/managecategories/classes/category_confition.php ' .'for more information.',DEBUG_DEVELOPER);global $DB, $OUTPUT;list($categoryid, $contextid) = explode(',', $categoryandcontext);if (!$categoryid) {$this->print_choose_category_message();return false;}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('Function display_options_form() is deprecated, this method has been replaced with mustaches in filters, ' .'please use filtering objects',DEBUG_DEVELOPER);global $PAGE;// The html will be refactored in the filter feature implementation.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;}}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('Function display_advanced_search_form() is deprecated, this method has been replaced with mustaches in ' .'filters, please use filtering objects',DEBUG_DEVELOPER);print_collapsible_region_start('', 'advancedsearch',get_string('advancedsearchoptions', 'question'),'question_bank_advanced_search');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);}/*** Does the current view allow adding new questions?** @return bool True if the view supports adding new questions.*/public function allow_add_questions(): bool {return true;}/*** Output the question bank controls for each plugin.** 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 get_plugin_controls(\core\context $context, int $categoryid): string {global $OUTPUT;$orderedcontrols = [];foreach ($this->plugins as $plugin) {$plugincontrols = $plugin->get_question_bank_controls($this, $context, $categoryid);foreach ($plugincontrols as $position => $plugincontrol) {if (!array_key_exists($position, $orderedcontrols)) {$orderedcontrols[$position] = [];}$orderedcontrols[$position][] = $plugincontrol;}}ksort($orderedcontrols);$output = '';foreach ($orderedcontrols as $controls) {foreach ($controls as $control) {$output .= $OUTPUT->render($control);}}return $OUTPUT->render_from_template('core_question/question_bank_controls', ['controls' => $output]);}/*** Prints the table of questions in a category with interactions*/public function display_question_list(): void {// 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);[$categoryid, $contextid] = category_condition::validate_category_param($this->pagevars['cat']);$catcontext = \context::instance_by_id($contextid);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);$this->build_query();$questionsrs = $this->load_page_questions();$totalquestions = $this->get_question_count();$questions = [];foreach ($questionsrs as $question) {if (!empty($question->id)) {$questions[$question->id] = $question;}}$questionsrs->close();// This html will be refactored in the bulk actions implementation.echo \html_writer::start_tag('form', ['action' => $this->baseurl, 'method' => 'post', 'id' => 'questionsubmit']);echo \html_writer::start_tag('fieldset', ['class' => 'invisiblefieldset', 'style' => "display: block;"]);echo \html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()]);echo \html_writer::input_hidden_params($this->baseurl);$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);// 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*/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*/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();$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*/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());// TODO MDL-82312: it really should not be necessary to set filter here, and not like this.// This should be handled in baseurl, but it isn't so we do this so Moodle basically works for now.$pageingurl->param('filter', json_encode($this->pagevars['filter']));$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' => 'question_table', 'id' => 'question_table']);$this->print_table($questions);echo \html_writer::end_tag('div');echo $OUTPUT->render($pagingbar);}/*** Load the questions according to the search conditions.** @return array*/public function load_questions() {$this->build_query();$questionsrs = $this->load_page_questions();$questions = [];foreach ($questionsrs as $question) {if (!empty($question->id)) {$questions[$question->id] = $question;}}$questionsrs->close();foreach ($this->requiredcolumns as $name => $column) {$column->load_additional_data($questions);}return $questions;}/*** Prints the actual table with question.** @param array $questions*/protected function print_table($questions): void {// Start of the table.echo \html_writer::start_tag('table', ['id' => 'categoryquestions','class' => 'question-bank-table generaltable','data-defaultsort' => json_encode($this->sort),]);// Prints the table header.echo \html_writer::start_tag('thead');echo \html_writer::start_tag('tr', ['class' => 'qbank-column-list']);$this->print_table_headers();echo \html_writer::end_tag('tr');echo \html_writer::end_tag('thead');// Prints the table row or content.echo \html_writer::start_tag('tbody');$rowcount = 0;foreach ($questions as $question) {$this->print_table_row($question, $rowcount);$rowcount += 1;}echo \html_writer::end_tag('tbody');// End of the table.echo \html_writer::end_tag('table');}/*** Start of the table html.** @see print_table()* @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.** @see print_table()* @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) {$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*/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);}}/*** @deprecated since Moodle 4.0*/public function process_actions(): void {throw new coding_exception(__FUNCTION__ . '() has been removed.');}/*** @deprecated since Moodle 4.0*/public function process_actions_needing_ui() {throw new coding_exception(__FUNCTION__ . '() has been removed.');}/*** Add another search control to this view.* @param condition $searchcondition the condition to add.* @param string|null $fieldname*/public function add_searchcondition(condition $searchcondition, ?string $fieldname = null): void {if (is_null($fieldname)) {$this->searchconditions[] = $searchcondition;} else {$this->searchconditions[$fieldname] = $searchcondition;}}/*** Add standard search conditions.* Params must be set into this object before calling this function.*/public function add_standard_search_conditions(): void {foreach ($this->plugins as $componentname => $plugin) {if (\core\plugininfo\qbank::is_plugin_enabled($componentname)) {$pluginentrypointobject = new $plugin();$pluginobjects = $pluginentrypointobject->get_question_filters($this);foreach ($pluginobjects as $pluginobject) {$this->add_searchcondition($pluginobject, $pluginobject->get_condition_key());}}}}/*** Gets visible columns.* @return array Visible columns.*/public function get_visiblecolumns(): array {return $this->visiblecolumns;}/*** Is this view showing separate versions of a question?** @return bool*/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;}}