<?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;