Commit dfed4fd0 authored by safatshahin's avatar safatshahin Committed by Safat Shahin
Browse files

MDL-71516 core_question: Qbank api implementation



This commit implements the qbank api so that any plugin
can implement its own question bank. This api currently
works parallely with the moodle core classes and the
added qbank in the core, means the moment a plugin
is installed, that object is replaced with the object
from the plugin instead of core, which means the api
has flexibility till the plugins are integrated and the
plugins can be integrated in any order.

All the old classes are still there and not deprecated
as there is a different tracker for the changes to the
quiz and another tracker for class deprecation and
class renaming. Core question units tests are pointing
to the new api structure but the classes are pointing
to the location related to the plugin availability.

Co-Authored-By: default avatarLuca Bösch <luca.boesch@bfh.ch>
Co-Authored-By: default avatarGuillermo Gomez Arias <guillermogomez@catalyst-au.net>

one more array fix
parent 351176bb
......@@ -36,7 +36,7 @@ $PAGE->set_context($syscontext);
require_admin();
$return = new moodle_url('/admin/settings.php', array('section' => 'manageqbanks'));
$return = new moodle_url('/admin/settings.php', ['section' => 'manageqbanks']);
$plugins = core_plugin_manager::instance()->get_plugins_of_type('qbank');
$sortorder = array_flip(array_keys($plugins));
......@@ -61,4 +61,3 @@ switch ($action) {
core_plugin_manager::reset_caches();
redirect($return);
......@@ -406,7 +406,7 @@ if ($hassiteconfig) {
// Question bank settings.
if ($hassiteconfig || has_capability('moodle/question:config', $systemcontext)) {
$ADMIN->add('modules', new admin_category('qbanksettings',
new lang_string('questionbanks', 'question')));
new lang_string('type_qbank_plural', 'plugin')));
$temp = new admin_settingpage('manageqbanks', new lang_string('manageqbanks', 'admin'));
$temp->add(new \core_question\admin\manage_qbank_plugins_page());
$ADMIN->add('qbanksettings', $temp);
......
......@@ -192,8 +192,8 @@ $string['type_tool'] = 'Admin tool';
$string['type_tool_plural'] = 'Admin tools';
$string['type_webservice'] = 'Webservice protocol';
$string['type_webservice_plural'] = 'Webservice protocols';
$string['type_qbank'] = 'Question bank';
$string['type_qbank_plural'] = 'Question banks';
$string['type_qbank'] = 'Question bank plugin';
$string['type_qbank_plural'] = 'Question bank plugins';
$string['updateavailable'] = 'There is a new version {$a} available!';
$string['updateavailable_moreinfo'] = 'More info...';
$string['updateavailable_release'] = 'Release {$a}';
......
......@@ -494,6 +494,5 @@ $string['whichtries'] = 'Which tries';
$string['withselected'] = 'With selected';
$string['xoutofmax'] = '{$a->mark} out of {$a->max}';
$string['yougotnright'] = 'You have correctly selected {$a->num}.';
$string['questionbanks'] = 'Question bank plugins';
$string['qbanknotfound'] = 'The \'{$a}\' question bank doesn\'t exist or is not recognised.';
$string['qbanknotfound'] = 'The \'{$a}\' question bank plugin doesn\'t exist or is not recognised.';
$string['noquestionbanks'] = 'No question bank plugin found.';
......@@ -1938,9 +1938,9 @@ class core_plugin_manager {
'checkbox', 'datetime', 'menu', 'social', 'text', 'textarea'
),
'qbank' => array(
'qbank' => [
''
),
],
'qbehaviour' => array(
'adaptive', 'adaptivenopenalty', 'deferredcbm',
......
......@@ -42,7 +42,7 @@ class qbank extends base {
}
public static function get_manage_url(): \moodle_url {
return new \moodle_url('/admin/settings.php', array('section' => 'manageqbanks'));
return new \moodle_url('/admin/settings.php', ['section' => 'manageqbanks']);
}
public static function get_plugins($type, $typerootdir, $typeclass, $pluginman): array {
......@@ -50,30 +50,26 @@ class qbank extends base {
$qbank = parent::get_plugins($type, $typerootdir, $typeclass, $pluginman);
$order = array_keys($qbank);
$sortedqbanks = array();
$sortedqbanks = [];
foreach ($order as $qbankname) {
$sortedqbanks[$qbankname] = $qbank[$qbankname];
}
return $sortedqbanks;
}
/**
* Finds all enabled plugins, the result may include missing plugins.
* @return array|null of enabled plugins $pluginname=>$pluginname, null means unknown
*/
public static function get_enabled_plugins(): ?array {
global $CFG;
$pluginmanager = \core_plugin_manager::instance();
$plugins = $pluginmanager->get_installed_plugins('qbank');
if (!$plugins) {
return array();
return [];
}
$plugins = array_keys($plugins);
// Filter to return only enabled plugins.
$enabled = array();
$enabled = [];
foreach ($plugins as $plugin) {
$qbankinfo = $pluginmanager->get_plugin_info('qbank_'.$plugin);
$qbankavailable = $qbankinfo->get_status();
......@@ -95,7 +91,7 @@ class qbank extends base {
* @param string $fullpluginname the name of the plugin
* @return bool
*/
public static function is_ready($fullpluginname): bool {
public static function is_plugin_enabled($fullpluginname): bool {
$pluginmanager = \core_plugin_manager::instance();
$qbankinfo = $pluginmanager->get_plugin_info($fullpluginname);
if (empty($qbankinfo)) {
......@@ -109,16 +105,6 @@ class qbank extends base {
return true;
}
/**
* Loads plugin settings to the settings tree
*
* This function usually includes settings.php file in plugins folder.
* Alternatively it can create a link to some settings page (instance of admin_externalpage)
*
* @param \part_of_admin_tree $adminroot
* @param string $parentnodename
* @param bool $hassiteconfig whether the current user has moodle/site:config capability
*/
public function load_settings(\part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig): void {
global $CFG, $USER, $DB, $OUTPUT, $PAGE; // In case settings.php wants to refer to them.
$ADMIN = $adminroot; // May be used in settings.php.
......
......@@ -1756,19 +1756,23 @@ function question_edit_url($context) {
}
/**
* Adds question bank setting links to the given navigation node if caps are met.
* Adds question bank setting links to the given navigation node if caps are met
* and loads the navigation from the plugins.
* Qbank plugins can extend the navigation_plugin_base and add their own navigation node,
* this method will help to autoload those nodes in the question bank navigation.
*
* @param navigation_node $navigationnode The navigation node to add the question branch to
* @param object $context
* @param string $baseurl the url of the base where the api is implemented from
* @return navigation_node Returns the question branch that was added
*/
function question_extend_settings_navigation(navigation_node $navigationnode, $context) {
function question_extend_settings_navigation(navigation_node $navigationnode, $context, $baseurl = '/question/edit.php') {
global $PAGE;
if ($context->contextlevel == CONTEXT_COURSE) {
$params = array('courseid'=>$context->instanceid);
$params = ['courseid' => $context->instanceid];
} else if ($context->contextlevel == CONTEXT_MODULE) {
$params = array('cmid'=>$context->instanceid);
$params = ['cmid' => $context->instanceid];
} else {
return;
}
......@@ -1778,24 +1782,84 @@ function question_extend_settings_navigation(navigation_node $navigationnode, $c
}
$questionnode = $navigationnode->add(get_string('questionbank', 'question'),
new moodle_url('/question/edit.php', $params), navigation_node::TYPE_CONTAINER, null, 'questionbank');
new moodle_url($baseurl, $params), navigation_node::TYPE_CONTAINER, null, 'questionbank');
$corenavigations = [
'questions' => [
'title' => get_string('questions', 'question'),
'url' => new moodle_url($baseurl)
],
'categories' => [
'title' => get_string('categories', 'question'),
'url' => new moodle_url('/question/category.php')
],
'import' => [
'title' => get_string('import', 'question'),
'url' => new moodle_url('/question/import.php')
],
'export' => [
'title' => get_string('export', 'question'),
'url' => new moodle_url('/question/export.php')
]
];
$plugins = \core_component::get_plugin_list_with_class('qbank', 'plugin_feature', 'plugin_feature.php');
foreach ($plugins as $componentname => $plugin) {
$pluginentrypoint = new $plugin();
$pluginentrypointobject = $pluginentrypoint->get_navigation_node();
// Don't need the plugins without navigation node.
if ($pluginentrypointobject === null) {
unset($plugins[$componentname]);
continue;
}
foreach ($corenavigations as $key => $corenavigation) {
if ($pluginentrypointobject->get_navigation_key() === $key) {
unset($plugins[$componentname]);
if (!\core\plugininfo\qbank::is_plugin_enabled($componentname)) {
unset($corenavigations[$key]);
break;
}
$corenavigations[$key] = [
'title' => $pluginentrypointobject->get_navigation_title(),
'url' => $pluginentrypointobject->get_navigation_url()
];
}
}
$contexts = new question_edit_contexts($context);
if ($contexts->have_one_edit_tab_cap('questions')) {
$questionnode->add(get_string('questions', 'question'), new moodle_url(
'/question/edit.php', $params), navigation_node::TYPE_SETTING, null, 'questions');
}
if ($contexts->have_one_edit_tab_cap('categories')) {
$questionnode->add(get_string('categories', 'question'), new moodle_url(
'/question/category.php', $params), navigation_node::TYPE_SETTING, null, 'categories');
// Community/additional plugins have navigation node.
$pluginnavigations = [];
foreach ($plugins as $componentname => $plugin) {
$pluginentrypoin = new $plugin();
$pluginentrypointobject = $pluginentrypoin->get_navigation_node();
if (!\core\plugininfo\qbank::is_plugin_enabled($componentname)) {
unset($corenavigations[$key]);
continue;
}
$pluginnavigations[$pluginentrypointobject->get_navigation_key()] = [
'title' => $pluginentrypointobject->get_navigation_title(),
'url' => $pluginentrypointobject->get_navigation_url(),
'capabilities' => $pluginentrypointobject->get_navigation_capabilities()
];
}
if ($contexts->have_one_edit_tab_cap('import')) {
$questionnode->add(get_string('import', 'question'), new moodle_url(
'/question/import.php', $params), navigation_node::TYPE_SETTING, null, 'import');
$contexts = new question_edit_contexts($context);
foreach ($corenavigations as $key => $corenavigation) {
if ($contexts->have_one_edit_tab_cap($key)) {
$questionnode->add($corenavigation['title'], new moodle_url(
$corenavigation['url'], $params), navigation_node::TYPE_SETTING, null, $key);
}
}
if ($contexts->have_one_edit_tab_cap('export')) {
$questionnode->add(get_string('export', 'question'), new moodle_url(
'/question/export.php', $params), navigation_node::TYPE_SETTING, null, 'export');
foreach ($pluginnavigations as $key => $pluginnavigation) {
if (is_array($pluginnavigation['capabilities'])) {
if (!$contexts->have_one_cap($pluginnavigation['capabilities'])) {
continue;
}
}
$questionnode->add($pluginnavigation['title'], new moodle_url(
$pluginnavigation['url'], $params), navigation_node::TYPE_SETTING, null, $key);
}
return $questionnode;
......
<?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/>.
/**
* A column type for the name of the question name.
*
* @package core_question
* @copyright 2009 Tim Hunt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\question\bank;
defined('MOODLE_INTERNAL') || die();
/**
* A column type for the name of the question name.
*
* @copyright 2009 Tim Hunt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @todo MDL-72004 delete the class and add it to lib/db/renameclasses.php pointing to the plugin
*/
class question_name_column extends \core_question\bank\column_base {
protected $checkboxespresent = null;
public function get_name() {
return 'questionname';
}
protected function get_title() {
return get_string('question');
}
protected function label_for($question) {
if (is_null($this->checkboxespresent)) {
$this->checkboxespresent = $this->qbank->has_column('core_question\bank\checkbox_column');
}
if ($this->checkboxespresent) {
return 'checkq' . $question->id;
} else {
return '';
}
}
protected function display_content($question, $rowclasses) {
$labelfor = $this->label_for($question);
if ($labelfor) {
echo '<label for="' . $labelfor . '">';
}
echo format_string($question->name);
if ($labelfor) {
echo '</label>';
}
}
public function get_required_fields() {
return array('q.id', 'q.name');
}
public function is_sortable() {
return 'q.name';
}
}
......@@ -33,7 +33,7 @@ defined('MOODLE_INTERNAL') || die();
* @copyright 2009 Tim Hunt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class question_name_text_column extends \core_question\bank\question_name_column {
class question_name_text_column extends question_name_column {
public function get_name() {
return 'questionnametext';
}
......
......@@ -133,14 +133,15 @@ class question_category_list_item extends list_item {
}
public function item_html($extraargs = array()){
global $CFG, $OUTPUT;
global $CFG, $PAGE, $OUTPUT;
$str = $extraargs['str'];
$category = $this->item;
$editqestions = get_string('editquestions', 'question');
// Each section adds html to be displayed as part of this list item.
$questionbankurl = new moodle_url('/question/edit.php', $this->parentlist->pageurl->params());
$nodeparent = $PAGE->settingsnav->find('questionbank', \navigation_node::TYPE_CONTAINER);
$questionbankurl = new moodle_url($nodeparent->action->get_path(), $this->parentlist->pageurl->params());
$questionbankurl->param('cat', $category->id . ',' . $category->contextid);
$item = '';
$text = format_string($category->name, true, ['context' => $this->parentlist->context]);
......
......@@ -25,8 +25,6 @@
namespace core_question\admin;
defined('MOODLE_INTERNAL') || die();
/**
* Class manage_qbank_plugins_page.
*
......@@ -81,14 +79,14 @@ class manage_qbank_plugins_page extends \admin_setting {
if (empty($types)) {
return get_string('noquestionbanks', 'question');
}
$txt = get_strings(array('settings', 'name', 'enable', 'disable', 'default'));
$txt = get_strings(['settings', 'name', 'enable', 'disable', 'default']);
$txt->uninstall = get_string('uninstallplugin', 'core_admin');
$table = new \html_table();
$table->head = array($txt->name, $txt->enable, $txt->settings, $txt->uninstall);
$table->align = array('left', 'center', 'center', 'center', 'center');
$table->head = [$txt->name, $txt->enable, $txt->settings, $txt->uninstall];
$table->align = ['left', 'center', 'center', 'center', 'center'];
$table->attributes['class'] = 'manageqbanktable generaltable admintable';
$table->data = array();
$table->data = [];
$totalenabled = 0;
$count = 0;
......@@ -99,8 +97,7 @@ class manage_qbank_plugins_page extends \admin_setting {
}
foreach ($types as $type) {
$url = new \moodle_url('/admin/qbankplugins.php',
array('sesskey' => sesskey(), 'name' => $type->name));
$url = new \moodle_url('/admin/qbankplugins.php', ['sesskey' => sesskey(), 'name' => $type->name]);
$class = '';
if ($pluginmanager->get_plugin_info('qbank_'.$type->name)->get_status() ===
......@@ -111,12 +108,12 @@ class manage_qbank_plugins_page extends \admin_setting {
}
if ($type->is_enabled()) {
$hideshow = \html_writer::link($url->out(false, array('action' => 'disable')),
$OUTPUT->pix_icon('t/hide', $txt->disable, 'moodle', array('class' => 'iconsmall')));
$hideshow = \html_writer::link($url->out(false, ['action' => 'disable']),
$OUTPUT->pix_icon('t/hide', $txt->disable, 'moodle', ['class' => 'iconsmall']));
} else {
$class = 'dimmed_text';
$hideshow = \html_writer::link($url->out(false, array('action' => 'enable')),
$OUTPUT->pix_icon('t/show', $txt->enable, 'moodle', array('class' => 'iconsmall')));
$hideshow = \html_writer::link($url->out(false, ['action' => 'enable']),
$OUTPUT->pix_icon('t/show', $txt->enable, 'moodle', ['class' => 'iconsmall']));
}
$settings = '';
......@@ -130,7 +127,7 @@ class manage_qbank_plugins_page extends \admin_setting {
$uninstall = \html_writer::link($uninstallurl, $txt->uninstall);
}
$row = new \html_table_row(array($strtypename, $hideshow, $settings, $uninstall));
$row = new \html_table_row([$strtypename, $hideshow, $settings, $uninstall]);
if ($class) {
$row->attributes['class'] = $class;
}
......
......@@ -46,7 +46,7 @@ class question_name_column extends column_base {
protected function label_for($question) {
if (is_null($this->checkboxespresent)) {
$this->checkboxespresent = $this->qbank->has_column('core_question\bank\checkbox_column');
$this->checkboxespresent = $this->qbank->has_column('core_question\local\bank\checkbox_column');
}
if ($this->checkboxespresent) {
return 'checkq' . $question->id;
......
......@@ -24,12 +24,12 @@
*/
namespace core_question\bank\search;
defined('MOODLE_INTERNAL') || die();
/**
* This class controls from which category questions are listed.
*
* @copyright 2013 Ray Morris
* @author 2021 Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class category_condition extends condition {
......@@ -87,16 +87,25 @@ class category_condition extends condition {
if ($this->recurse) {
$categoryids = question_categorylist($this->category->id);
} else {
$categoryids = array($this->category->id);
$categoryids = [$this->category->id];
}
list($catidtest, $this->params) = $DB->get_in_or_equal($categoryids, SQL_PARAMS_NAMED, 'cat');
$this->where = 'q.category ' . $catidtest;
}
/**
* SQL fragment to add to the where clause.
*
* @return string
*/
public function where() {
return $this->where;
}
/**
* Return parameters to be bound to the above WHERE clause fragment.
* @return array parameter name => value.
*/
public function params() {
return $this->params;
}
......@@ -105,8 +114,14 @@ class category_condition extends condition {
* Called by question_bank_view to display the GUI for selecting a category
*/
public function display_options() {
$this->display_category_form($this->contexts, $this->baseurl, $this->cat);
$this->print_category_info($this->category);
global $PAGE;
$displaydata = [];
$catmenu = question_category_options($this->contexts, true, 0,
true, -1, false);
$displaydata['categoryselect'] = \html_writer::select($catmenu, 'category', $this->cat, [],
array('class' => 'searchoptions custom-select', 'id' => 'id_selectacategory'));
$displaydata['categorydesc'] = $this->print_category_info($this->category);
return $PAGE->get_renderer('core_question', 'bank')->render_category_condition($displaydata);
}
/**
......@@ -114,12 +129,12 @@ class category_condition extends condition {
* question_bank_view places this within the section that is hidden by default
*/
public function display_options_adv() {
echo \html_writer::start_div();
echo \html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'recurse',
'value' => 0, 'id' => 'recurse_off'));
echo \html_writer::checkbox('recurse', '1', $this->recurse, get_string('includesubcategories', 'question'),
array('id' => 'recurse_on', 'class' => 'searchoptions mr-1'));
echo \html_writer::end_div() . "\n";
global $PAGE;
$displaydata = [];
if ($this->recurse) {
$displaydata['checked'] = 'checked="true"';
}
return $PAGE->get_renderer('core_question', 'bank')->render_category_condition_advanced($displaydata);
}
/**
......@@ -128,12 +143,15 @@ class category_condition extends condition {
* @param array $contexts of contexts that can be accessed from here.
* @param \moodle_url $pageurl the URL of this page.
* @param string $current 'categoryID,contextID'.
* @deprecated since Moodle 4.0
*/
protected function display_category_form($contexts, $pageurl, $current) {
debugging('Function display_category_form() is deprecated,
please use the core_question renderer instead.', DEBUG_DEVELOPER);
echo \html_writer::start_div('choosecategory');
$catmenu = question_category_options($contexts, true, 0, true, -1, false);
echo \html_writer::label(get_string('selectacategory', 'question'), 'id_selectacategory', true, array("class" => "mr-1"));
echo \html_writer::select($catmenu, 'category', $current, array(),
echo \html_writer::label(get_string('selectacategory', 'question'), 'id_selectacategory', true, ["class" => "mr-1"]);
echo \html_writer::select($catmenu, 'category', $current, [],
array('class' => 'searchoptions custom-select', 'id' => 'id_selectacategory'));
echo \html_writer::end_div() . "\n";
}
......@@ -151,8 +169,7 @@ class category_condition extends condition {
return false;
}
if (!$category = $DB->get_record('question_categories',
array('id' => $categoryid, 'contextid' => $contextid))) {
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();
......@@ -164,19 +181,17 @@ class category_condition extends condition {
/**
* Print the category description
* @param stdClass $category the category information form the database.
* @param \stdClass $category the category information form the database.
*/
protected function print_category_info($category) {
protected function print_category_info($category): string {
$formatoptions = new \stdClass();
$formatoptions->noclean = true;
$formatoptions->overflowdiv = true;
echo \html_writer::start_div('boxaligncenter categoryinfo pl-0');
if (isset($this->maxinfolength)) {
echo shorten_text(format_text($category->info, $category->infoformat, $formatoptions, $this->course->id),
$this->maxinfolength);
return shorten_text(format_text($category->info, $category->infoformat, $formatoptions, $this->course->id),
$this->maxinfolength);
} else {
echo format_text($category->info, $category->infoformat, $formatoptions, $this->course->id);
return format_text($category->info, $category->infoformat, $formatoptions, $this->course->id);
}
echo \html_writer::end_div() . "\n";
}
}
......@@ -24,12 +24,11 @@
*/
namespace core_question\bank\search;
defined('MOODLE_INTERNAL') || die();
/**
* An abstract class for filtering/searching questions.
*
* See also {@link question_bank_view::init_search_conditions()}.
* See also {@see question_bank_view::init_search_conditions()}.
* @copyright 2013 Ray Morris
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
......@@ -38,33 +37,33 @@ abstract class condition {
* Return an SQL fragment to be ANDed into the WHERE clause to filter which questions are shown.
* @return string SQL fragment. Must use named parameters.
*/
public abstract function where();
abstract public function where();
/**
* Return parameters to be bound to the above WHERE clause fragment.
* @return array parameter name => value.
*/
public function params() {
return array();
return [];
}
/**
* Display GUI for selecting criteria for this condition. Displayed when Show More is open.
*