diff --git a/admin/qbankplugins.php b/admin/qbankplugins.php new file mode 100644 index 00000000000..e3b71a10157 --- /dev/null +++ b/admin/qbankplugins.php @@ -0,0 +1,63 @@ +. + +/** + * Question bank plugin settings. + * + * @package core + * @subpackage questionbank + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once('../config.php'); +require_once($CFG->libdir.'/adminlib.php'); + +$action = required_param('action', PARAM_ALPHANUMEXT); +$name = required_param('name', PARAM_PLUGIN); + +$syscontext = context_system::instance(); +$PAGE->set_url('/admin/qbankplugins.php'); +$PAGE->set_context($syscontext); + +require_admin(); + +$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)); + +if (!isset($plugins[$name])) { + throw new moodle_exception('qbanknotfound', 'question', $return, $name); +} + +switch ($action) { + case 'disable': + if ($plugins[$name]->is_enabled()) { + set_config('disabled', 1, 'qbank_'. $name); + } + break; + case 'enable': + if (!$plugins[$name]->is_enabled()) { + unset_config('disabled', 'qbank_'. $name); + } + break; +} + +core_plugin_manager::reset_caches(); + +redirect($return); diff --git a/admin/settings/plugins.php b/admin/settings/plugins.php index 7c75aa2e2da..0135a172339 100644 --- a/admin/settings/plugins.php +++ b/admin/settings/plugins.php @@ -403,6 +403,20 @@ if ($hassiteconfig) { } } +// Question bank settings. +if ($hassiteconfig || has_capability('moodle/question:config', $systemcontext)) { + $ADMIN->add('modules', new admin_category('qbanksettings', + 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); + $plugins = core_plugin_manager::instance()->get_plugins_of_type('qbank'); + foreach ($plugins as $plugin) { + /** @var \core\plugininfo\qbank $plugin */ + $plugin->load_settings($ADMIN, 'qbanksettings', $hassiteconfig); + } +} + // Question type settings if ($hassiteconfig || has_capability('moodle/question:config', $systemcontext)) { diff --git a/lang/en/admin.php b/lang/en/admin.php index d45a8bd1dd9..1575e8c3e07 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -1515,6 +1515,7 @@ $string['webproxyinfo'] = 'Fill in the following options if your Moodle server c $string['xmlrpcrecommended'] = 'The XMLRPC extension is useful for web services and Moodle networking.'; $string['yuicomboloading'] = 'YUI combo loading'; $string['ziprequired'] = 'The Zip PHP extension is now required by Moodle, info-ZIP binaries or PclZip library are not used anymore.'; +$string['manageqbanks'] = 'Manage question bank plugins'; $string['caching'] = 'Caching'; diff --git a/lang/en/plugin.php b/lang/en/plugin.php index 0820cb8643b..92c88fae602 100644 --- a/lang/en/plugin.php +++ b/lang/en/plugin.php @@ -192,6 +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 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}'; diff --git a/lang/en/question.php b/lang/en/question.php index b7dde536a8f..ef2e66a6258 100644 --- a/lang/en/question.php +++ b/lang/en/question.php @@ -494,3 +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['qbanknotfound'] = 'The \'{$a}\' question bank plugin doesn\'t exist or is not recognised.'; +$string['noquestionbanks'] = 'No question bank plugin found.'; diff --git a/lib/classes/plugin_manager.php b/lib/classes/plugin_manager.php index de8401c7e42..c399e6924a4 100644 --- a/lib/classes/plugin_manager.php +++ b/lib/classes/plugin_manager.php @@ -1938,6 +1938,10 @@ class core_plugin_manager { 'checkbox', 'datetime', 'menu', 'social', 'text', 'textarea' ), + 'qbank' => [ + '' + ], + 'qbehaviour' => array( 'adaptive', 'adaptivenopenalty', 'deferredcbm', 'deferredfeedback', 'immediatecbm', 'immediatefeedback', @@ -2241,6 +2245,7 @@ class core_plugin_manager { $fix['mod'] = $types['mod']; $fix['block'] = $types['block']; $fix['qtype'] = $types['qtype']; + $fix['qbank'] = $types['qbank']; $fix['qbehaviour'] = $types['qbehaviour']; $fix['qformat'] = $types['qformat']; $fix['filter'] = $types['filter']; diff --git a/lib/classes/plugininfo/qbank.php b/lib/classes/plugininfo/qbank.php new file mode 100644 index 00000000000..d2e8540e446 --- /dev/null +++ b/lib/classes/plugininfo/qbank.php @@ -0,0 +1,133 @@ +. + +/** + * Defines classes used for plugin info. + * + * @package core + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\plugininfo; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Base class for qbank plugins. + * + * @package core + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qbank extends base { + + public function is_uninstall_allowed(): bool { + return true; + } + + public static function get_manage_url(): \moodle_url { + return new \moodle_url('/admin/settings.php', ['section' => 'manageqbanks']); + } + + public static function get_plugins($type, $typerootdir, $typeclass, $pluginman): array { + global $CFG; + + $qbank = parent::get_plugins($type, $typerootdir, $typeclass, $pluginman); + $order = array_keys($qbank); + $sortedqbanks = []; + foreach ($order as $qbankname) { + $sortedqbanks[$qbankname] = $qbank[$qbankname]; + } + return $sortedqbanks; + } + + public static function get_enabled_plugins(): ?array { + global $CFG; + $pluginmanager = \core_plugin_manager::instance(); + $plugins = $pluginmanager->get_installed_plugins('qbank'); + + if (!$plugins) { + return []; + } + + $plugins = array_keys($plugins); + + // Filter to return only enabled plugins. + $enabled = []; + foreach ($plugins as $plugin) { + $qbankinfo = $pluginmanager->get_plugin_info('qbank_'.$plugin); + $qbankavailable = $qbankinfo->get_status(); + if ($qbankavailable === \core_plugin_manager::PLUGIN_STATUS_MISSING) { + continue; + } + $disabled = get_config('qbank_' . $plugin, 'disabled'); + if (empty($disabled)) { + $enabled[$plugin] = $plugin; + } + } + return $enabled; + } + + /** + * Checks if a qbank plugin is ready to be used. + * It checks the plugin status as well as the plugin is missing or not. + * + * @param string $fullpluginname the name of the plugin + * @return bool + */ + public static function is_plugin_enabled($fullpluginname): bool { + $pluginmanager = \core_plugin_manager::instance(); + $qbankinfo = $pluginmanager->get_plugin_info($fullpluginname); + if (empty($qbankinfo)) { + return false; + } + $qbankavailable = $qbankinfo->get_status(); + if ($qbankavailable === \core_plugin_manager::PLUGIN_STATUS_MISSING || + !empty(get_config($fullpluginname, 'disabled'))) { + return false; + } + return true; + } + + 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. + $plugininfo = $this; // Also can be used inside settings.php. + + if (!$this->is_installed_and_upgraded()) { + return; + } + + if (!$hassiteconfig) { + return; + } + + $section = $this->get_settings_section_name(); + + $settings = null; + if (file_exists($this->full_path('settings.php'))) { + $settings = new \admin_settingpage($section, $this->displayname, + 'moodle/site:config', $this->is_enabled() === false); + include($this->full_path('settings.php')); // This may also set $settings to null. + } + if ($settings) { + $ADMIN->add($parentnodename, $settings); + } + } +} diff --git a/lib/components.json b/lib/components.json index a9a635fd6ef..73dea36498e 100644 --- a/lib/components.json +++ b/lib/components.json @@ -28,6 +28,7 @@ "repository": "repository", "portfolio": "portfolio", "search": "search\/engine", + "qbank": "question\/bank", "qbehaviour": "question\/behaviour", "qformat": "question\/format", "plagiarism": "plagiarism", diff --git a/lib/questionlib.php b/lib/questionlib.php index b703fe71096..1bf8900da84 100644 --- a/lib/questionlib.php +++ b/lib/questionlib.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() + ]; + } + } + + } + + // 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() + ]; + } $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'); + 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('categories')) { - $questionnode->add(get_string('categories', 'question'), new moodle_url( - '/question/category.php', $params), navigation_node::TYPE_SETTING, null, 'categories'); - } - 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'); - } - 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; diff --git a/mod/quiz/classes/question/bank/question_name_column.php b/mod/quiz/classes/question/bank/question_name_column.php new file mode 100644 index 00000000000..43ff13d4476 --- /dev/null +++ b/mod/quiz/classes/question/bank/question_name_column.php @@ -0,0 +1,76 @@ +. + +/** + * 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 ''; + } + } + + public function get_required_fields() { + return array('q.id', 'q.name'); + } + + public function is_sortable() { + return 'q.name'; + } +} diff --git a/mod/quiz/classes/question/bank/question_name_text_column.php b/mod/quiz/classes/question/bank/question_name_text_column.php index ecde32767f0..65e8437e62f 100644 --- a/mod/quiz/classes/question/bank/question_name_text_column.php +++ b/mod/quiz/classes/question/bank/question_name_text_column.php @@ -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'; } diff --git a/question/bank/upgrade.txt b/question/bank/upgrade.txt new file mode 100644 index 00000000000..a7836ce8c92 --- /dev/null +++ b/question/bank/upgrade.txt @@ -0,0 +1,2 @@ +This file describes core qbank plugin changes in /question/bank/*, +information provided here is intended especially for developers. diff --git a/question/category_class.php b/question/category_class.php index 1713cadea5c..843eb0b5837 100644 --- a/question/category_class.php +++ b/question/category_class.php @@ -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]); diff --git a/question/classes/admin/manage_qbank_plugins_page.php b/question/classes/admin/manage_qbank_plugins_page.php new file mode 100644 index 00000000000..e6b4dafa8c8 --- /dev/null +++ b/question/classes/admin/manage_qbank_plugins_page.php @@ -0,0 +1,140 @@ +. + +/** + * Manage question banks page. + * + * @package core_question + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_question\admin; + +/** + * Class manage_qbank_plugins_page. + * + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class manage_qbank_plugins_page extends \admin_setting { + + /** + * Class admin_page_manageqbanks constructor. + */ + public function __construct() { + $this->nosave = true; + parent::__construct('manageqbanks', + new \lang_string('manageqbanks', 'admin'), '', ''); + } + + public function get_setting(): bool { + return true; + } + + public function get_defaultsetting(): bool { + return true; + } + + public function write_setting($data): string { + // Do not write any setting. + return ''; + } + + public function is_related($query): bool { + if (parent::is_related($query)) { + return true; + } + $types = \core_plugin_manager::instance()->get_plugins_of_type('qbank'); + foreach ($types as $type) { + if (strpos($type->component, $query) !== false || + strpos(\core_text::strtolower($type->displayname), $query) !== false) { + return true; + } + } + return false; + } + + public function output_html($data, $query = ''): string { + global $CFG, $OUTPUT; + $return = ''; + + $pluginmanager = \core_plugin_manager::instance(); + $types = $pluginmanager->get_plugins_of_type('qbank'); + if (empty($types)) { + return get_string('noquestionbanks', 'question'); + } + $txt = get_strings(['settings', 'name', 'enable', 'disable', 'default']); + $txt->uninstall = get_string('uninstallplugin', 'core_admin'); + + $table = new \html_table(); + $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 = []; + + $totalenabled = 0; + $count = 0; + foreach ($types as $type) { + if ($type->is_enabled() && $type->is_installed_and_upgraded()) { + $totalenabled++; + } + } + + foreach ($types as $type) { + $url = new \moodle_url('/admin/qbankplugins.php', ['sesskey' => sesskey(), 'name' => $type->name]); + + $class = ''; + if ($pluginmanager->get_plugin_info('qbank_'.$type->name)->get_status() === + \core_plugin_manager::PLUGIN_STATUS_MISSING) { + $strtypename = $type->displayname.' ('.get_string('missingfromdisk').')'; + } else { + $strtypename = $type->displayname; + } + + if ($type->is_enabled()) { + $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, ['action' => 'enable']), + $OUTPUT->pix_icon('t/show', $txt->enable, 'moodle', ['class' => 'iconsmall'])); + } + + $settings = ''; + if ($type->get_settings_url()) { + $settings = \html_writer::link($type->get_settings_url(), $txt->settings); + } + + $uninstall = ''; + if ($uninstallurl = \core_plugin_manager::instance()->get_uninstall_url( + 'qbank_'.$type->name, 'manage')) { + $uninstall = \html_writer::link($uninstallurl, $txt->uninstall); + } + + $row = new \html_table_row([$strtypename, $hideshow, $settings, $uninstall]); + if ($class) { + $row->attributes['class'] = $class; + } + $table->data[] = $row; + $count++; + } + $return .= \html_writer::table($table); + return highlight($query, $return); + } +} diff --git a/question/classes/bank/action_column_base.php b/question/classes/bank/action_column_base.php index 2c34e41dedb..b5043c874c7 100644 --- a/question/classes/bank/action_column_base.php +++ b/question/classes/bank/action_column_base.php @@ -31,6 +31,7 @@ defined('MOODLE_INTERNAL') || die(); * * @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 core_question/local/bank */ abstract class action_column_base extends column_base { diff --git a/question/classes/bank/checkbox_column.php b/question/classes/bank/checkbox_column.php index 9b2f628b4fe..18e59510055 100644 --- a/question/classes/bank/checkbox_column.php +++ b/question/classes/bank/checkbox_column.php @@ -32,6 +32,7 @@ use core\output\checkbox_toggleall; * * @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 core_question/local/bank */ class checkbox_column extends column_base { diff --git a/question/classes/bank/column_base.php b/question/classes/bank/column_base.php index 8bacb784a38..3a7fe62f1ac 100644 --- a/question/classes/bank/column_base.php +++ b/question/classes/bank/column_base.php @@ -15,7 +15,7 @@ // along with Moodle. If not, see . /** - * Base class for representing a column in a {@link question_bank_view}. + * Base class for representing a column in a {@see question_bank_view}. * * @package core_question * @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com} @@ -27,12 +27,14 @@ defined('MOODLE_INTERNAL') || die(); /** - * Base class for representing a column in a {@link question_bank_view}. + * Base class for representing a column in a {@see question_bank_view}. * * @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 core_question/local/bank */ abstract class column_base { + /** * @var view $qbank the question bank view we are helping to render. */ @@ -42,10 +44,10 @@ abstract class column_base { protected $isheading = false; /** - * Constructor. - * @param view $qbank the question bank view we are helping to render. + * Constructor for the question bank view we are helping to render. + * @param view $qbank */ - public function __construct(view $qbank) { + public function __construct($qbank) { $this->qbank = $qbank; $this->init(); } @@ -68,6 +70,27 @@ abstract class column_base { return false; } + /** + * Check if the row has an extra preference to view/hide. + */ + public function has_preference(): bool { + return false; + } + + /** + * Get if the preference key of the row. + */ + public function get_preference_key(): string { + return ''; + } + + /** + * Get if the preference of the row. + */ + public function get_preference(): bool { + return false; + } + /** * Output the column header cell. */ @@ -272,7 +295,7 @@ abstract class column_base { /** * Load the tags for each question. * - * Helper that can be used from {@link load_additional_data()}; + * Helper that can be used from {@see load_additional_data()}; * * @param array $questions */ diff --git a/question/classes/bank/copy_action_column.php b/question/classes/bank/copy_action_column.php index ef5e6f00409..769b81ac26a 100644 --- a/question/classes/bank/copy_action_column.php +++ b/question/classes/bank/copy_action_column.php @@ -31,6 +31,7 @@ defined('MOODLE_INTERNAL') || die(); * * @copyright 2013 The Open University * @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 copy_action_column extends menu_action_column_base { /** @var string avoids repeated calls to get_string('duplicate'). */ diff --git a/question/classes/bank/creator_name_column.php b/question/classes/bank/creator_name_column.php index c6d5b263ba1..4db687f6936 100644 --- a/question/classes/bank/creator_name_column.php +++ b/question/classes/bank/creator_name_column.php @@ -31,6 +31,7 @@ defined('MOODLE_INTERNAL') || die(); * * @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 creator_name_column extends column_base { public function get_name() { diff --git a/question/classes/bank/delete_action_column.php b/question/classes/bank/delete_action_column.php index 3e5419b1139..f969dcc84f9 100644 --- a/question/classes/bank/delete_action_column.php +++ b/question/classes/bank/delete_action_column.php @@ -31,6 +31,7 @@ defined('MOODLE_INTERNAL') || die(); * * @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 delete_action_column extends menu_action_column_base { protected $strdelete; diff --git a/question/classes/bank/edit_action_column.php b/question/classes/bank/edit_action_column.php index b41b487281c..582ce80ae49 100644 --- a/question/classes/bank/edit_action_column.php +++ b/question/classes/bank/edit_action_column.php @@ -31,6 +31,7 @@ defined('MOODLE_INTERNAL') || die(); * * @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 edit_action_column extends menu_action_column_base { protected $stredit; diff --git a/question/classes/bank/edit_menu_column.php b/question/classes/bank/edit_menu_column.php index 072abe1f2b6..687d0b8546e 100644 --- a/question/classes/bank/edit_menu_column.php +++ b/question/classes/bank/edit_menu_column.php @@ -36,6 +36,7 @@ defined('MOODLE_INTERNAL') || die(); * * @copyright 2019 The Open University * @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 core_question/local/bank */ class edit_menu_column extends column_base { /** diff --git a/question/classes/bank/export_xml_action_column.php b/question/classes/bank/export_xml_action_column.php index 08a9dc55ca9..6807a994d08 100644 --- a/question/classes/bank/export_xml_action_column.php +++ b/question/classes/bank/export_xml_action_column.php @@ -31,6 +31,7 @@ defined('MOODLE_INTERNAL') || die(); * * @copyright 2019 The Open University * @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 export_xml_action_column extends menu_action_column_base { /** @var string avoids repeated calls to get_string('duplicate'). */ diff --git a/question/classes/bank/menu_action_column_base.php b/question/classes/bank/menu_action_column_base.php index 5a0b52da58b..c730bd7dfdc 100644 --- a/question/classes/bank/menu_action_column_base.php +++ b/question/classes/bank/menu_action_column_base.php @@ -36,6 +36,7 @@ defined('MOODLE_INTERNAL') || die(); * * @copyright 2019 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 core_question/local/bank */ abstract class menu_action_column_base extends action_column_base implements menuable_action { diff --git a/question/classes/bank/menuable_action.php b/question/classes/bank/menuable_action.php index 0a0e76f2fff..2a4278cc157 100644 --- a/question/classes/bank/menuable_action.php +++ b/question/classes/bank/menuable_action.php @@ -43,6 +43,7 @@ defined('MOODLE_INTERNAL') || die(); * * @copyright 2019 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 core_question/local/bank */ interface menuable_action { diff --git a/question/classes/bank/modifier_name_column.php b/question/classes/bank/modifier_name_column.php index 35d8617c120..3b7621ff287 100644 --- a/question/classes/bank/modifier_name_column.php +++ b/question/classes/bank/modifier_name_column.php @@ -31,6 +31,7 @@ defined('MOODLE_INTERNAL') || die(); * * @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 modifier_name_column extends column_base { public function get_name() { diff --git a/question/classes/bank/preview_action_column.php b/question/classes/bank/preview_action_column.php index e10e7b02441..54469e0e2ed 100644 --- a/question/classes/bank/preview_action_column.php +++ b/question/classes/bank/preview_action_column.php @@ -31,6 +31,7 @@ defined('MOODLE_INTERNAL') || die(); * * @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 preview_action_column extends action_column_base implements menuable_action { /** diff --git a/question/classes/bank/question_name_column.php b/question/classes/bank/question_name_column.php index 528a5ca236d..2fd578b2449 100644 --- a/question/classes/bank/question_name_column.php +++ b/question/classes/bank/question_name_column.php @@ -31,6 +31,7 @@ defined('MOODLE_INTERNAL') || die(); * * @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 column_base { protected $checkboxespresent = null; @@ -45,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; diff --git a/question/classes/bank/question_name_idnumber_tags_column.php b/question/classes/bank/question_name_idnumber_tags_column.php index 85cea05cda3..d026a0dc40b 100644 --- a/question/classes/bank/question_name_idnumber_tags_column.php +++ b/question/classes/bank/question_name_idnumber_tags_column.php @@ -31,6 +31,7 @@ defined('MOODLE_INTERNAL') || die(); * * @copyright 2019 The Open University * @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_idnumber_tags_column extends question_name_column { public function get_name() { diff --git a/question/classes/bank/question_text_row.php b/question/classes/bank/question_text_row.php index b05b0a7c099..626083999a0 100644 --- a/question/classes/bank/question_text_row.php +++ b/question/classes/bank/question_text_row.php @@ -31,6 +31,7 @@ defined('MOODLE_INTERNAL') || die(); * * @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_text_row extends row_base { protected $formatoptions; diff --git a/question/classes/bank/question_type_column.php b/question/classes/bank/question_type_column.php index 064ce95ecbc..cf2f18ecf43 100644 --- a/question/classes/bank/question_type_column.php +++ b/question/classes/bank/question_type_column.php @@ -31,6 +31,7 @@ defined('MOODLE_INTERNAL') || die(); * * @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_type_column extends column_base { public function get_name() { diff --git a/question/classes/bank/random_question_loader.php b/question/classes/bank/random_question_loader.php index 4fcbcfb6b50..9dac3ac9128 100644 --- a/question/classes/bank/random_question_loader.php +++ b/question/classes/bank/random_question_loader.php @@ -41,6 +41,7 @@ namespace core_question\bank; * * @copyright 2015 The Open University * @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 core_question/local/bank */ class random_question_loader { /** @var \qubaid_condition which usages to consider previous attempts from. */ diff --git a/question/classes/bank/row_base.php b/question/classes/bank/row_base.php index 457899ed51c..7f9363ca40b 100644 --- a/question/classes/bank/row_base.php +++ b/question/classes/bank/row_base.php @@ -31,6 +31,7 @@ defined('MOODLE_INTERNAL') || die(); * * @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 core_question/local/bank */ abstract class row_base extends column_base { public function is_extra_row() { diff --git a/question/classes/bank/search/category_condition.php b/question/classes/bank/search/category_condition.php index f9b9cc9b3c7..27bb096a3cf 100644 --- a/question/classes/bank/search/category_condition.php +++ b/question/classes/bank/search/category_condition.php @@ -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 * @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"; } } diff --git a/question/classes/bank/search/condition.php b/question/classes/bank/search/condition.php index 34c2a5fe8d8..5e6b8475bde 100644 --- a/question/classes/bank/search/condition.php +++ b/question/classes/bank/search/condition.php @@ -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. * * Compare display_options(), which displays always, whether Show More is open or not. - * @return string HTML form fragment + * @return bool|string HTML form fragment */ public function display_options_adv() { - return; + return false; } /** * Display GUI for selecting criteria for this condition. Displayed always, whether Show More is open or not. * * Compare display_options_adv(), which displays when Show More is open. - * @return string HTML form fragment + * @return bool|string HTML form fragment */ public function display_options() { - return; + return false; } } diff --git a/question/classes/bank/search/hidden_condition.php b/question/classes/bank/search/hidden_condition.php index cf80f2eeb38..758240a7a3b 100644 --- a/question/classes/bank/search/hidden_condition.php +++ b/question/classes/bank/search/hidden_condition.php @@ -24,12 +24,12 @@ */ namespace core_question\bank\search; -defined('MOODLE_INTERNAL') || die(); /** * This class controls whether hidden / deleted questions are hidden in the list. * * @copyright 2013 Ray Morris + * @author 2021 Safat Shahin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class hidden_condition extends condition { @@ -50,6 +50,11 @@ class hidden_condition extends condition { } } + /** + * SQL fragment to add to the where clause. + * + * @return string + */ public function where() { return $this->where; } @@ -58,11 +63,11 @@ class hidden_condition extends condition { * Print HTML to display the "Also show old questions" checkbox */ public function display_options_adv() { - echo \html_writer::start_div(); - echo \html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'showhidden', - 'value' => '0', 'id' => 'showhidden_off')); - echo \html_writer::checkbox('showhidden', '1', (! $this->hide), get_string('showhidden', 'question'), - array('id' => 'showhidden_on', 'class' => 'searchoptions mr-1')); - echo \html_writer::end_div() . "\n"; + global $PAGE; + $displaydata = []; + if (!$this->hide) { + $displaydata['checked'] = 'checked="true"'; + } + return $PAGE->get_renderer('core_question', 'bank')->render_hidden_condition_advanced($displaydata); } } diff --git a/question/classes/bank/search/tag_condition.php b/question/classes/bank/search/tag_condition.php index 2a337a50d2c..521553ba18e 100644 --- a/question/classes/bank/search/tag_condition.php +++ b/question/classes/bank/search/tag_condition.php @@ -23,7 +23,6 @@ */ namespace core_question\bank\search; -defined('MOODLE_INTERNAL') || die(); /** * Question bank search class to allow searching/filtering by tags on a question. @@ -32,17 +31,23 @@ defined('MOODLE_INTERNAL') || die(); * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tag_condition extends condition { + /** @var string SQL fragment to add to the where clause. */ protected $where; + /** @var string SQL fragment to add to the where clause. */ protected $contexts; + /** @var array List of IDs for tags that have been selected in the form. */ protected $selectedtagids; /** * Constructor. - * @param context[] $contexts List of contexts to show tags from + * + * @param array $contexts List of contexts to show tags from * @param int[] $selectedtagids List of IDs for tags to filter by. + * @throws \coding_exception + * @throws \dml_exception */ public function __construct(array $contexts, array $selectedtagids = []) { global $DB; @@ -118,6 +123,6 @@ class tag_condition extends condition { 'tagoptions' => $tagoptions ]; - echo $OUTPUT->render_from_template('core_question/tag_condition', $context); + return $OUTPUT->render_from_template('core_question/tag_condition', $context); } } diff --git a/question/classes/bank/tags_action_column.php b/question/classes/bank/tags_action_column.php index 6899d2d2ac6..222ac580b36 100644 --- a/question/classes/bank/tags_action_column.php +++ b/question/classes/bank/tags_action_column.php @@ -30,6 +30,7 @@ defined('MOODLE_INTERNAL') || die(); * * @copyright 2018 Simey Lameze * @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 tags_action_column extends action_column_base implements menuable_action { /** @@ -39,6 +40,11 @@ class tags_action_column extends action_column_base implements menuable_action { public function init() { parent::init(); + global $CFG; + if ($CFG->usetags) { + global $PAGE; + $PAGE->requires->js_call_amd('core_question/edit_tags', 'init', ['#questionscontainer']); + } $this->managetags = get_string('managetags', 'tag'); } diff --git a/question/classes/bank/view.php b/question/classes/bank/view.php index de74be8c03e..711fba5607f 100644 --- a/question/classes/bank/view.php +++ b/question/classes/bank/view.php @@ -48,6 +48,7 @@ use core_question\bank\search\condition; * * @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 core_question/local/bank */ class view { const MAX_SORTS = 3; diff --git a/question/classes/external.php b/question/classes/external.php index f17818440f6..9bda45d7c54 100644 --- a/question/classes/external.php +++ b/question/classes/external.php @@ -285,7 +285,7 @@ class core_question_external extends external_api { // The user must be able to view all questions in the category that they are requesting. $editcontexts->require_cap('moodle/question:viewall'); - $loader = new \core_question\bank\random_question_loader(new qubaid_list([])); + $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([])); // Only load the properties we require from the DB. $properties = \core_question\external\question_summary_exporter::get_mandatory_properties(); $questions = $loader->get_questions($categoryid, $includesubcategories, $tagids, $limit, $offset, $properties); diff --git a/question/classes/local/bank/action_column_base.php b/question/classes/local/bank/action_column_base.php new file mode 100644 index 00000000000..a3e1f2248cf --- /dev/null +++ b/question/classes/local/bank/action_column_base.php @@ -0,0 +1,60 @@ +. + +/** + * A base class for actions that are an icon that lets you manipulate the question in some way. + * + * @package core_question + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_question\local\bank; + +/** + * A base class for actions that are an icon that lets you manipulate the question in some way. + * + * @copyright 2009 Tim Hunt + * @author 2021 Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class action_column_base extends column_base { + + protected function get_title(): string { + return ' '; + } + + public function get_extra_classes(): array { + return ['iconcol']; + } + + protected function print_icon($icon, $title, $url): void { + global $OUTPUT; + echo \html_writer::tag('a', $OUTPUT->pix_icon($icon, $title), ['title' => $title, 'href' => $url]); + } + + public function get_extra_joins(): array { + return ['qc' => 'JOIN {question_categories} qc ON qc.id = q.category']; + } + + public function get_required_fields(): array { + // Createdby is required for permission checks. + // Qtype so we can easily avoid applying actions to question types that + // are no longer installed. + return ['q.id', 'q.qtype', 'q.createdby', 'qc.contextid']; + } + +} diff --git a/question/classes/local/bank/checkbox_column.php b/question/classes/local/bank/checkbox_column.php new file mode 100644 index 00000000000..babd720c828 --- /dev/null +++ b/question/classes/local/bank/checkbox_column.php @@ -0,0 +1,78 @@ +. + +/** + * A column with a checkbox for each question with name q{questionid}. + * + * @package core_question + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_question\local\bank; + +use core\output\checkbox_toggleall; + +/** + * A column with a checkbox for each question with name q{questionid}. + * + * @copyright 2009 Tim Hunt + * @author 2021 Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class checkbox_column extends column_base { + + public function get_name(): string { + return 'checkbox'; + } + + protected function get_title() { + global $OUTPUT; + + $mastercheckbox = new checkbox_toggleall('qbank', true, [ + 'id' => 'qbheadercheckbox', + 'name' => 'qbheadercheckbox', + 'value' => '1', + 'label' => get_string('selectall'), + 'labelclasses' => 'accesshide', + ]); + + return $OUTPUT->render($mastercheckbox); + } + + protected function get_title_tip() { + return get_string('selectquestionsforbulk', 'question'); + } + + protected function display_content($question, $rowclasses): void { + global $OUTPUT; + + $checkbox = new checkbox_toggleall('qbank', false, [ + 'id' => "checkq{$question->id}", + 'name' => "q{$question->id}", + 'value' => '1', + 'label' => get_string('select'), + 'labelclasses' => 'accesshide', + ]); + + echo $OUTPUT->render($checkbox); + } + + public function get_required_fields(): array { + return ['q.id']; + } + +} diff --git a/question/classes/local/bank/column_base.php b/question/classes/local/bank/column_base.php new file mode 100644 index 00000000000..24aa63c76c1 --- /dev/null +++ b/question/classes/local/bank/column_base.php @@ -0,0 +1,393 @@ +. + +/** + * Base class for representing a column. + * + * @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; + +/** + * Base class for representing a column. + * + * @copyright 2009 Tim Hunt + * @author 2021 Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class column_base { + + /** + * @var view $qbank the question bank view we are helping to render. + */ + protected $qbank; + + /** @var bool determine whether the column is td or th. */ + protected $isheading = false; + + /** + * Constructor. + * @param view $qbank the question bank view we are helping to render. + */ + public function __construct(view $qbank) { + $this->qbank = $qbank; + $this->init(); + } + + /** + * A chance for subclasses to initialise themselves, for example to load lang strings, + * without having to override the constructor. + */ + protected function init(): void { + } + + /** + * Set the column as heading + */ + public function set_as_heading(): void { + $this->isheading = true; + } + + /** + * Check if the column is an extra row of not. + */ + public function is_extra_row(): bool { + return false; + } + + /** + * Check if the row has an extra preference to view/hide. + */ + public function has_preference(): bool { + return false; + } + + /** + * Get if the preference key of the row. + */ + public function get_preference_key(): string { + return ''; + } + + /** + * Get if the preference of the row. + */ + public function get_preference(): bool { + return false; + } + + /** + * Output the column header cell. + */ + public function display_header(): void { + global $PAGE; + + $data = []; + $data['sortable'] = true; + $data['extraclasses'] = $this->get_classes(); + $sortable = $this->is_sortable(); + $name = get_class($this); + $title = $this->get_title(); + $tip = $this->get_title_tip(); + $links = []; + if (is_array($sortable)) { + if ($title) { + $data['title'] = $title; + } + foreach ($sortable as $subsort => $details) { + $links[] = $this->make_sort_link($name . '-' . $subsort, + $details['title'], isset($details['tip']) ? $details['tip'] : '', !empty($details['reverse'])); + } + $data['sortlinks'] = implode(' / ', $links); + } else if ($sortable) { + $data['sortlinks'] = $this->make_sort_link($name, $title, $tip); + } else { + $data['sortable'] = false; + $data['tiptitle'] = $title; + if ($tip) { + $data['sorttip'] = true; + $data['tip'] = $tip; + } + } + + $renderer = $PAGE->get_renderer('core_question', 'bank'); + echo $renderer->render_column_header($data); + } + + /** + * Title for this column. Not used if is_sortable returns an array. + */ + abstract protected function get_title(); + + /** + * Use this when get_title() returns + * something very short, and you want a longer version as a tool tip. + * + * @return string a fuller version of the name. + */ + protected function get_title_tip() { + return ''; + } + + /** + * Get a link that changes the sort order, and indicates the current sort state. + * @param string $sort the column to sort on. + * @param string $title the link text. + * @param string $tip the link tool-tip text. If empty, defaults to title. + * @param bool $defaultreverse whether the default sort order for this column is descending, rather than ascending. + * @return string + */ + protected function make_sort_link($sort, $title, $tip, $defaultreverse = false): string { + global $PAGE; + $sortdata = []; + $currentsort = $this->qbank->get_primary_sort_order($sort); + $newsortreverse = $defaultreverse; + if ($currentsort) { + $newsortreverse = $currentsort > 0; + } + if (!$tip) { + $tip = $title; + } + if ($newsortreverse) { + $tip = get_string('sortbyxreverse', '', $tip); + } else { + $tip = get_string('sortbyx', '', $tip); + } + + $link = $title; + if ($currentsort) { + $link .= $this->get_sort_icon($currentsort < 0); + } + + $sortdata['sorturl'] = $this->qbank->new_sort_url($sort, $newsortreverse); + $sortdata['sortcontent'] = $link; + $sortdata['sorttip'] = $tip; + $renderer = $PAGE->get_renderer('core_question', 'bank'); + return $renderer->render_column_sort($sortdata); + + } + + /** + * Get an icon representing the corrent sort state. + * @param bool $reverse sort is descending, not ascending. + * @return string HTML image tag. + */ + protected function get_sort_icon($reverse): string { + global $OUTPUT; + if ($reverse) { + return $OUTPUT->pix_icon('t/sort_desc', get_string('desc'), '', ['class' => 'iconsort']); + } else { + return $OUTPUT->pix_icon('t/sort_asc', get_string('asc'), '', ['class' => 'iconsort']); + } + } + + /** + * Output this column. + * @param object $question the row from the $question table, augmented with extra information. + * @param string $rowclasses CSS class names that should be applied to this row of output. + */ + public function display($question, $rowclasses): void { + $this->display_start($question, $rowclasses); + $this->display_content($question, $rowclasses); + $this->display_end($question, $rowclasses); + } + + /** + * Output the opening column tag. If it is set as heading, it will use tag instead of + * + * @param \stdClass $question + * @param string $rowclasses + */ + protected function display_start($question, $rowclasses): void { + $tag = 'td'; + $attr = ['class' => $this->get_classes()]; + if ($this->isheading) { + $tag = 'th'; + $attr['scope'] = 'row'; + } + echo \html_writer::start_tag($tag, $attr); + } + + /** + * The CSS classes to apply to every cell in this column. + * + * @return string + */ + protected function get_classes(): string { + $classes = $this->get_extra_classes(); + $classes[] = $this->get_name(); + return implode(' ', $classes); + } + + /** + * Get the internal name for this column. Used as a CSS class name, + * and to store information about the current sort. Must match PARAM_ALPHA. + * + * @return string column name. + */ + abstract public function get_name(); + + /** + * Any extra class names you would like applied to every cell in this column. + * + * @return array + */ + public function get_extra_classes(): array { + return []; + } + + /** + * Output the contents of this column. + * @param object $question the row from the $question table, augmented with extra information. + * @param string $rowclasses CSS class names that should be applied to this row of output. + */ + abstract protected function display_content($question, $rowclasses); + + /** + * Output the closing column tag + * + * @param object $question + * @param string $rowclasses + */ + protected function display_end($question, $rowclasses): void { + $tag = 'td'; + if ($this->isheading) { + $tag = 'th'; + } + echo \html_writer::end_tag($tag); + } + + /** + * Return an array 'table_alias' => 'JOIN clause' to bring in any data that + * this column required. + * + * The return values for all the columns will be checked. It is OK if two + * columns join in the same table with the same alias and identical JOIN clauses. + * If to columns try to use the same alias with different joins, you get an error. + * The only table included by default is the question table, which is aliased to 'q'. + * + * It is importnat that your join simply adds additional data (or NULLs) to the + * existing rows of the query. It must not cause additional rows. + * + * @return array 'table_alias' => 'JOIN clause' + */ + public function get_extra_joins(): array { + return []; + } + + /** + * Use table alias 'q' for the question table, or one of the + * ones from get_extra_joins. Every field requested must specify a table prefix. + * + * @return array fields required. + */ + public function get_required_fields(): array { + return []; + } + + /** + * If this column needs extra data (e.g. tags) then load that here. + * + * The extra data should be added to the question object in the array. + * Probably a good idea to check that another column has not already + * loaded the data you want. + * + * @param \stdClass[] $questions the questions that will be displayed. + */ + public function load_additional_data(array $questions) { + } + + /** + * Load the tags for each question. + * + * Helper that can be used from {@see load_additional_data()}; + * + * @param array $questions + */ + public function load_question_tags(array $questions): void { + $firstquestion = reset($questions); + if (isset($firstquestion->tags)) { + // Looks like tags are already loaded, so don't do it again. + return; + } + + // Load the tags. + $tagdata = \core_tag_tag::get_items_tags('core_question', 'question', + array_keys($questions)); + + // Add them to the question objects. + foreach ($tagdata as $questionid => $tags) { + $questions[$questionid]->tags = $tags; + } + } + + /** + * Can this column be sorted on? You can return either: + * + false for no (the default), + * + a field name, if sorting this column corresponds to sorting on that datbase field. + * + an array of subnames to sort on as follows + * return [ + * 'firstname' => ['field' => 'uc.firstname', 'title' => get_string('firstname')], + * 'lastname' => ['field' => 'uc.lastname', 'title' => get_string('lastname')], + * ]; + * As well as field, and field, you can also add 'revers' => 1 if you want the default sort + * order to be DESC. + * @return mixed as above. + */ + public function is_sortable() { + return false; + } + + /** + * Helper method for building sort clauses. + * @param bool $reverse whether the normal direction should be reversed. + * @return string 'ASC' or 'DESC' + */ + protected function sortorder($reverse): string { + if ($reverse) { + return ' DESC'; + } else { + return ' ASC'; + } + } + + /** + * Sorts the expressions. + * + * @param bool $reverse Whether to sort in the reverse of the default sort order. + * @param string $subsort if is_sortable returns an array of subnames, then this will be + * one of those. Otherwise will be empty. + * @return string some SQL to go in the order by clause. + */ + public function sort_expression($reverse, $subsort): string { + $sortable = $this->is_sortable(); + if (is_array($sortable)) { + if (array_key_exists($subsort, $sortable)) { + return $sortable[$subsort]['field'] . $this->sortorder($reverse); + } else { + throw new \coding_exception('Unexpected $subsort type: ' . $subsort); + } + } else if ($sortable) { + return $sortable . $this->sortorder($reverse); + } else { + throw new \coding_exception('sort_expression called on a non-sortable column.'); + } + } + +} diff --git a/question/classes/local/bank/edit_menu_column.php b/question/classes/local/bank/edit_menu_column.php new file mode 100644 index 00000000000..ea061d70755 --- /dev/null +++ b/question/classes/local/bank/edit_menu_column.php @@ -0,0 +1,102 @@ +. + +/** + * A question bank column which gathers together all the actions into a menu. + * + * @package core_question + * @copyright 2019 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_question\local\bank; + +/** + * A question bank column which gathers together all the actions into a menu. + * + * This question bank column, if added to the question bank, will + * replace all of the other columns which implement the + * {@see menuable_action} interface and replace them with a single + * column containing an Edit menu. + * + * @copyright 2019 The Open University + * @author 2021 Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class edit_menu_column extends column_base { + /** + * @var menuable_action[] + */ + protected $actions; + + /** + * Set up the list of actions that should be shown in the menu. + * + * This takes a list of column object (the list from a question + * bank view). It extracts all the ones that should go in the menu + * and stores them for later use. Then it returns the remaining columns. + * + * @param column_base[] $allcolumns a set of columns. + * @return column_base[] the non-action columns from the set. + * @todo MDL-72004 changes for class renaming. + */ + public function claim_menuable_columns($allcolumns): array { + $remainingcolumns = []; + foreach ($allcolumns as $key => $column) { + if ($column instanceof menuable_action || $column instanceof \core_question\bank\menuable_action) { + $this->actions[$key] = $column; + } else { + $remainingcolumns[$key] = $column; + } + } + return $remainingcolumns; + } + + protected function get_title() { + return get_string('actions'); + } + + public function get_name(): string { + return 'editmenu'; + } + + protected function display_content($question, $rowclasses): void { + global $OUTPUT; + + $menu = new \action_menu(); + $menu->set_menu_trigger(get_string('edit')); + $menu->set_alignment(\action_menu::TL, \action_menu::BL); + foreach ($this->actions as $actioncolumn) { + $action = $actioncolumn->get_action_menu_link($question); + if ($action) { + $menu->add($action); + } + } + + $qtypeactions = \question_bank::get_qtype($question->qtype, false) + ->get_extra_question_bank_actions($question); + foreach ($qtypeactions as $action) { + $menu->add($action); + } + + echo $OUTPUT->render($menu); + } + + public function get_required_fields():array { + return ['q.qtype']; + } + +} diff --git a/question/classes/local/bank/helper.php b/question/classes/local/bank/helper.php new file mode 100644 index 00000000000..90ec3e3861c --- /dev/null +++ b/question/classes/local/bank/helper.php @@ -0,0 +1,55 @@ +. + +/** + * Helper class for question bank and its plugins. + * + * All the functions which has a potential to be used by different features or + * plugins, should go here. + * + * @package core_question + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_question\local\bank; + +/** + * Class helper + * + * @package core_question + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class helper { + + /** + * Check the status of a plugin and throw exception if not enabled and called manually. + * + * Any action plugin having a php script, should call this function for a safer enable/disable implementation. + * + * @param string $pluginname + * @return void + */ + public static function require_plugin_enabled(string $pluginname): void { + if (!\core\plugininfo\qbank::is_plugin_enabled($pluginname)) { + throw new \moodle_exception('The following plugin is either disabled or missing from disk: ' . $pluginname); + } + } + +} diff --git a/question/classes/local/bank/menu_action_column_base.php b/question/classes/local/bank/menu_action_column_base.php new file mode 100644 index 00000000000..e74ce57f5c6 --- /dev/null +++ b/question/classes/local/bank/menu_action_column_base.php @@ -0,0 +1,69 @@ +. + +/** + * Base class to make it easier to implement actions that are menuable_actions. + * + * @package core_question + * @copyright 2019 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_question\local\bank; + +/** + * Base class to make it easier to implement actions that are menuable_actions. + * + * Use this class if your action is simple (defined by just a URL, label and icon). + * If your action is not simple enough to fit into the pattern that this + * class implements, then you will have to implement the menuable_action + * interface yourself. + * + * @copyright 2019 Tim Hunt + * @author 2021 Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class menu_action_column_base extends action_column_base implements menuable_action { + + /** + * Get the information required to display this action either as a menu item or a separate action column. + * + * If this action cannot apply to this question (e.g. because the user does not have + * permission, then return [null, null, null]. + * + * @param \stdClass $question the row from the $question table, augmented with extra information. + * @return array with three elements. + * $url - the URL to perform the action. + * $icon - the icon for this action. E.g. 't/delete'. + * $label - text label to display in the UI (either in the menu, or as a tool-tip on the icon) + */ + abstract protected function get_url_icon_and_label(\stdClass $question); + + protected function display_content($question, $rowclasses): void { + [$url, $icon, $label] = $this->get_url_icon_and_label($question); + if ($url) { + $this->print_icon($icon, $label, $url); + } + } + + public function get_action_menu_link(\stdClass $question): ?\action_menu_link { + [$url, $icon, $label] = $this->get_url_icon_and_label($question); + if (!$url) { + return null; + } + return new \action_menu_link_secondary($url, new \pix_icon($icon, ''), $label); + } +} diff --git a/question/classes/local/bank/menuable_action.php b/question/classes/local/bank/menuable_action.php new file mode 100644 index 00000000000..698b2353590 --- /dev/null +++ b/question/classes/local/bank/menuable_action.php @@ -0,0 +1,54 @@ +. + +/** + * Interface to indicate that a question bank column can go in the action menu. + * + * @package core_question + * @copyright 2019 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_question\local\bank; + +/** + * Interface to indicate that a question bank column can go in the action menu. + * + * If a question bank column implements this interface, and if the {@see edit_menu_column} + * is present in the question bank view, then the 'column' will be shown as an entry in the + * edit menu instead of as a separate column. + * + * Probably most columns that want to implement this will be subclasses of + * {@see action_column_base}, and most such columns should probably implement + * this interface. + * + * If your column is a simple action, you can probably save duplicated code by + * using the base class action_column_menuable as an easy way to implement both + * action_column_base and this interface. + * + * @copyright 2019 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +interface menuable_action { + + /** + * Return the appropriate action menu link, or null if it does not apply to this question. + * + * @param \stdClass $question data about the question being displayed in this row. + * @return \action_menu_link|null the action, if applicable to this question. + */ + public function get_action_menu_link(\stdClass $question): ?\action_menu_link; +} diff --git a/question/classes/local/bank/navigation_node_base.php b/question/classes/local/bank/navigation_node_base.php new file mode 100644 index 00000000000..a8fcaf39aef --- /dev/null +++ b/question/classes/local/bank/navigation_node_base.php @@ -0,0 +1,64 @@ +. + +/** + * Base class class for navigation node. + * + * @package core_question + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_question\local\bank; + +/** + * Class navigation_node_base is the base class for navigation node. + * + * @package core_question + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class navigation_node_base { + + /** + * Title for this node. + */ + abstract public function get_navigation_title(); + + /** + * Key for this node. + */ + abstract public function get_navigation_key(); + + /** + * URL for this node + */ + abstract public function get_navigation_url(); + + /** + * Tab capabilities. + * + * If it has capabilities to be checked, it will return the array of capabilities. + * + * @return null|array + */ + public function get_navigation_capabilities(): ?array { + return null; + } + +} diff --git a/question/classes/local/bank/plugin_features_base.php b/question/classes/local/bank/plugin_features_base.php new file mode 100644 index 00000000000..6c8bccde55f --- /dev/null +++ b/question/classes/local/bank/plugin_features_base.php @@ -0,0 +1,59 @@ +. + +/** + * Base class class for qbank plugins. + * + * Every qbank plugin must extent this class. + * + * @package core_question + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_question\local\bank; + +/** + * Class plugin_features_base is the base class for qbank plugins. + * + * @package core_question + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class plugin_features_base { + + /** + * This method will return the array of objects to be rendered as a prt of question bank columns/actions. + * + * @param view $qbank + * @return array + */ + public function get_question_columns(view $qbank): ?array { + return []; + } + + /** + * This method will return the object for the navigation node. + * + * @return null|object + */ + public function get_navigation_node(): ?object { + return null; + } + +} diff --git a/question/classes/local/bank/random_question_loader.php b/question/classes/local/bank/random_question_loader.php new file mode 100644 index 00000000000..829aa2607db --- /dev/null +++ b/question/classes/local/bank/random_question_loader.php @@ -0,0 +1,310 @@ +. + +/** + * A class for efficiently finds questions at random from the question bank. + * + * @package core_question + * @copyright 2015 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_question\local\bank; + +/** + * This class efficiently finds questions at random from the question bank. + * + * You can ask for questions at random one at a time. Each time you ask, you + * pass a category id, and whether to pick from that category and all subcategories + * or just that category. + * + * The number of teams each question has been used is tracked, and we will always + * return a question from among those elegible that has been used the fewest times. + * So, if there are questions that have not been used yet in the category asked for, + * one of those will be returned. However, within one instantiation of this class, + * we will never return a given question more than once, and we will never return + * questions passed into the constructor as $usedquestions. + * + * @copyright 2015 The Open University + * @author 2021 Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class random_question_loader { + /** @var \qubaid_condition which usages to consider previous attempts from. */ + protected $qubaids; + + /** @var array qtypes that cannot be used by random questions. */ + protected $excludedqtypes; + + /** @var array categoryid & include subcategories => num previous uses => questionid => 1. */ + protected $availablequestionscache = []; + + /** + * @var array questionid => num recent uses. Questions that have been used, + * but that is not yet recorded in the DB. + */ + protected $recentlyusedquestions; + + /** + * Constructor. + * + * @param \qubaid_condition $qubaids the usages to consider when counting previous uses of each question. + * @param array $usedquestions questionid => number of times used count. If we should allow for + * further existing uses of a question in addition to the ones in $qubaids. + */ + public function __construct(\qubaid_condition $qubaids, array $usedquestions = []) { + $this->qubaids = $qubaids; + $this->recentlyusedquestions = $usedquestions; + + foreach (\question_bank::get_all_qtypes() as $qtype) { + if (!$qtype->is_usable_by_random()) { + $this->excludedqtypes[] = $qtype->name(); + } + } + } + + /** + * Pick a question at random from the given category, from among those with the fewest uses. + * If an array of tag ids are specified, then only the questions that are tagged with ALL those tags will be selected. + * + * It is up the the caller to verify that the cateogry exists. An unknown category + * behaves like an empty one. + * + * @param int $categoryid the id of a category in the question bank. + * @param bool $includesubcategories wether to pick a question from exactly + * that category, or that category and subcategories. + * @param array $tagids An array of tag ids. A question has to be tagged with all the provided tagids (if any) + * in order to be eligible for being picked. + * @return int|null the id of the question picked, or null if there aren't any. + */ + public function get_next_question_id($categoryid, $includesubcategories, $tagids = []): ?int { + $this->ensure_questions_for_category_loaded($categoryid, $includesubcategories, $tagids); + + $categorykey = $this->get_category_key($categoryid, $includesubcategories, $tagids); + if (empty($this->availablequestionscache[$categorykey])) { + return null; + } + + reset($this->availablequestionscache[$categorykey]); + $lowestcount = key($this->availablequestionscache[$categorykey]); + reset($this->availablequestionscache[$categorykey][$lowestcount]); + $questionid = key($this->availablequestionscache[$categorykey][$lowestcount]); + $this->use_question($questionid); + return $questionid; + } + + /** + * Get the key into {@see $availablequestionscache} for this combination of options. + * + * @param int $categoryid the id of a category in the question bank. + * @param bool $includesubcategories wether to pick a question from exactly + * that category, or that category and subcategories. + * @param array $tagids an array of tag ids. + * @return string the cache key. + */ + protected function get_category_key($categoryid, $includesubcategories, $tagids = []): string { + if ($includesubcategories) { + $key = $categoryid . '|1'; + } else { + $key = $categoryid . '|0'; + } + + if (!empty($tagids)) { + $key .= '|' . implode('|', $tagids); + } + + return $key; + } + + /** + * Populate {@see $availablequestionscache} for this combination of options. + * + * @param int $categoryid The id of a category in the question bank. + * @param bool $includesubcategories Whether to pick a question from exactly + * that category, or that category and subcategories. + * @param array $tagids An array of tag ids. If an array is provided, then + * only the questions that are tagged with ALL the provided tagids will be loaded. + */ + protected function ensure_questions_for_category_loaded($categoryid, $includesubcategories, $tagids = []): void { + global $DB; + + $categorykey = $this->get_category_key($categoryid, $includesubcategories, $tagids); + + if (isset($this->availablequestionscache[$categorykey])) { + // Data is already in the cache, nothing to do. + return; + } + + // Load the available questions from the question bank. + if ($includesubcategories) { + $categoryids = question_categorylist($categoryid); + } else { + $categoryids = [$categoryid]; + } + + list($extraconditions, $extraparams) = $DB->get_in_or_equal($this->excludedqtypes, + SQL_PARAMS_NAMED, 'excludedqtype', false); + + $questionidsandcounts = \question_bank::get_finder()->get_questions_from_categories_and_tags_with_usage_counts( + $categoryids, $this->qubaids, 'q.qtype ' . $extraconditions, $extraparams, $tagids); + if (!$questionidsandcounts) { + // No questions in this category. + $this->availablequestionscache[$categorykey] = []; + return; + } + + // Put all the questions with each value of $prevusecount in separate arrays. + $idsbyusecount = []; + foreach ($questionidsandcounts as $questionid => $prevusecount) { + if (isset($this->recentlyusedquestions[$questionid])) { + // Recently used questions are never returned. + continue; + } + $idsbyusecount[$prevusecount][] = $questionid; + } + + // Now put that data into our cache. For each count, we need to shuffle + // questionids, and make those the keys of an array. + $this->availablequestionscache[$categorykey] = []; + foreach ($idsbyusecount as $prevusecount => $questionids) { + shuffle($questionids); + $this->availablequestionscache[$categorykey][$prevusecount] = array_combine( + $questionids, array_fill(0, count($questionids), 1)); + } + ksort($this->availablequestionscache[$categorykey]); + } + + /** + * Update the internal data structures to indicate that a given question has + * been used one more time. + * + * @param int $questionid the question that is being used. + */ + protected function use_question($questionid): void { + if (isset($this->recentlyusedquestions[$questionid])) { + $this->recentlyusedquestions[$questionid] += 1; + } else { + $this->recentlyusedquestions[$questionid] = 1; + } + + foreach ($this->availablequestionscache as $categorykey => $questionsforcategory) { + foreach ($questionsforcategory as $numuses => $questionids) { + if (!isset($questionids[$questionid])) { + continue; + } + unset($this->availablequestionscache[$categorykey][$numuses][$questionid]); + if (empty($this->availablequestionscache[$categorykey][$numuses])) { + unset($this->availablequestionscache[$categorykey][$numuses]); + } + } + } + } + + /** + * Get the list of available question ids for the given criteria. + * + * @param int $categoryid The id of a category in the question bank. + * @param bool $includesubcategories Whether to pick a question from exactly + * that category, or that category and subcategories. + * @param array $tagids An array of tag ids. If an array is provided, then + * only the questions that are tagged with ALL the provided tagids will be loaded. + * @return int[] The list of question ids + */ + protected function get_question_ids($categoryid, $includesubcategories, $tagids = []): array { + $this->ensure_questions_for_category_loaded($categoryid, $includesubcategories, $tagids); + $categorykey = $this->get_category_key($categoryid, $includesubcategories, $tagids); + $cachedvalues = $this->availablequestionscache[$categorykey]; + $questionids = []; + + foreach ($cachedvalues as $usecount => $ids) { + $questionids = array_merge($questionids, array_keys($ids)); + } + + return $questionids; + } + + /** + * Check whether a given question is available in a given category. If so, mark it used. + * If an optional list of tag ids are provided, then the question must be tagged with + * ALL of the provided tags to be considered as available. + * + * @param int $categoryid the id of a category in the question bank. + * @param bool $includesubcategories wether to pick a question from exactly + * that category, or that category and subcategories. + * @param int $questionid the question that is being used. + * @param array $tagids An array of tag ids. Only the questions that are tagged with all the provided tagids can be available. + * @return bool whether the question is available in the requested category. + */ + public function is_question_available($categoryid, $includesubcategories, $questionid, $tagids = []): bool { + $this->ensure_questions_for_category_loaded($categoryid, $includesubcategories, $tagids); + $categorykey = $this->get_category_key($categoryid, $includesubcategories, $tagids); + + foreach ($this->availablequestionscache[$categorykey] as $questionids) { + if (isset($questionids[$questionid])) { + $this->use_question($questionid); + return true; + } + } + + return false; + } + + /** + * Get the list of available questions for the given criteria. + * + * @param int $categoryid The id of a category in the question bank. + * @param bool $includesubcategories Whether to pick a question from exactly + * that category, or that category and subcategories. + * @param array $tagids An array of tag ids. If an array is provided, then + * only the questions that are tagged with ALL the provided tagids will be loaded. + * @param int $limit Maximum number of results to return. + * @param int $offset Number of items to skip from the begging of the result set. + * @param string[] $fields The fields to return for each question. + * @return \stdClass[] The list of question records + */ + public function get_questions($categoryid, $includesubcategories, $tagids = [], $limit = 100, $offset = 0, $fields = []) { + global $DB; + + $questionids = $this->get_question_ids($categoryid, $includesubcategories, $tagids); + if (empty($questionids)) { + return []; + } + + if (empty($fields)) { + // Return all fields. + $fieldsstring = '*'; + } else { + $fieldsstring = implode(',', $fields); + } + + return $DB->get_records_list('question', 'id', $questionids, 'id', $fieldsstring, $offset, $limit); + } + + /** + * Count the number of available questions for the given criteria. + * + * @param int $categoryid The id of a category in the question bank. + * @param bool $includesubcategories Whether to pick a question from exactly + * that category, or that category and subcategories. + * @param array $tagids An array of tag ids. If an array is provided, then + * only the questions that are tagged with ALL the provided tagids will be loaded. + * @return int The number of questions matching the criteria. + */ + public function count_questions($categoryid, $includesubcategories, $tagids = []): int { + $questionids = $this->get_question_ids($categoryid, $includesubcategories, $tagids); + return count($questionids); + } +} diff --git a/question/classes/local/bank/row_base.php b/question/classes/local/bank/row_base.php new file mode 100644 index 00000000000..d3d8a70cae8 --- /dev/null +++ b/question/classes/local/bank/row_base.php @@ -0,0 +1,70 @@ +. + +/** + * Base class for 'columns' that are actually displayed as a row following the main question row. + * + * @package core_question + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_question\local\bank; + +/** + * Base class for 'columns' that are actually displayed as a row following the main question row. + * + * @copyright 2009 Tim Hunt + * @author 2021 Safat Shahin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class row_base extends column_base { + + /** + * Check if the column is an extra row of not. + */ + public function is_extra_row(): bool { + return true; + } + + /** + * Output the opening column tag. If it is set as heading, it will use tag instead of + * + * @param \stdClass $question + * @param string $rowclasses + */ + protected function display_start($question, $rowclasses): void { + if ($rowclasses) { + echo \html_writer::start_tag('tr', ['class' => $rowclasses]); + } else { + echo \html_writer::start_tag('tr'); + } + echo \html_writer::start_tag('td', + ['colspan' => $this->qbank->get_column_count(), 'class' => $this->get_name()]); + } + + /** + * Output the closing column tag + * + * @param object $question + * @param string $rowclasses + */ + protected function display_end($question, $rowclasses): void { + echo \html_writer::end_tag('td'); + echo \html_writer::end_tag('tr'); + } + +} diff --git a/question/classes/local/bank/view.php b/question/classes/local/bank/view.php new file mode 100644 index 00000000000..24665b7102f --- /dev/null +++ b/question/classes/local/bank/view.php @@ -0,0 +1,1322 @@ +. + +/** + * 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_question\bank\search\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 + * @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 \question_edit_contexts + */ + protected $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 \question_bank_column_base[] these are all the 'columns' that are + * part of the display. Array keys are the class name. + */ + protected $requiredcolumns; + + /** + * @var \question_bank_column_base[] these are the 'columns' that are + * actually displayed as a column, in order. Array keys are the class name. + */ + protected $visiblecolumns; + + /** + * @var \question_bank_column_base[] these are the 'columns' that are + * actually displayed as an additional row (e.g. question text), in order. + * Array keys are the class name. + */ + protected $extrarows; + + /** + * @var array list of column class names for which columns to sort on. + */ + protected $sort; + + /** + * @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 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. + */ + public $enablefilters = true; + + /** + * @var array to pass custom filters instead of the specified ones. + */ + public $customfilterobjects = null; + + /** + * Constructor for view. + * + * @param \question_edit_contexts $contexts + * @param \moodle_url $pageurl + * @param object $course course settings + * @param object $cm (optional) activity settings. + */ + public function __construct($contexts, $pageurl, $course, $cm = null) { + $this->contexts = $contexts; + $this->baseurl = $pageurl; + $this->course = $course; + $this->cm = $cm; + + // Create the url of the new question page to forward to. + $this->returnurl = $pageurl->out_as_local_url(false); + $this->editquestionurl = new \moodle_url('/question/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 = optional_param('lastchanged', 0, PARAM_INT); + + // Possibly the heading part can be removed. + $this->init_columns($this->wanted_columns(), $this->heading_column()); + $this->init_sort(); + $this->init_search_conditions(); + } + + /** + * Initialize search conditions from plugins + * local_*_get_question_bank_search_conditions() must return an array of + * \core_question\bank\search\condition objects. + */ + protected function init_search_conditions(): void { + $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); + } + } + } + + /** + * Get the list of qbank plugins with available objects for features. + * + * @return array + * @todo MDL-72004 changes for class renaming. + */ + 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', + 'delete_action_column', + 'export_xml_action_column', + 'creator_name_column', + 'modifier_name_column' + ]; + if (question_get_display_preference('qbshowtext', 0, PARAM_BOOL, new \moodle_url(''))) { + $corequestionbankcolumns[] = 'question_text_row'; + } + + foreach ($corequestionbankcolumns as $fullname) { + $shortname = $fullname; + if (!class_exists($fullname)) { + if (class_exists('core_question\\local\\bank\\' . $fullname)) { + $fullname = 'core_question\\local\\bank\\' . $fullname; + } else if (class_exists('core_question\\bank\\' . $fullname)) { + $fullname = 'core_question\\bank\\' . $fullname; + } else { + throw new \coding_exception("No such class exists: $fullname"); + } + } + $questionbankclasscolumns[$shortname] = new $fullname($this); + } + $plugins = \core_component::get_plugin_list_with_class('qbank', 'plugin_feature', 'plugin_feature.php'); + foreach ($plugins as $componentname => $plugin) { + $pluginentrypointobject = new $plugin(); + $pluginobjects = $pluginentrypointobject->get_question_columns($this); + // Don't need the plugins without column objects. + if (empty($pluginobjects)) { + unset($plugins[$componentname]); + continue; + } + foreach ($pluginobjects as $pluginobject) { + $classname = new \ReflectionClass(get_class($pluginobject)); + foreach ($corequestionbankcolumns as $key => $corequestionbankcolumn) { + if (!\core\plugininfo\qbank::is_plugin_enabled($componentname)) { + unset($questionbankclasscolumns[$classname->getShortName()]); + continue; + } + // Check if it has custom preference selector to view/hide. + if ($pluginobject->has_preference()) { + if (!$pluginobject->get_preference()) { + continue; + } + } + if ($corequestionbankcolumn === $classname->getShortName()) { + $questionbankclasscolumns[$classname->getShortName()] = $pluginobject; + } else { + // Any community plugin for column/action. + $newpluginclasscolumns[$classname->getShortName()] = $pluginobject; + } + } + } + } + + // New plugins added at the end of the array, will change in sorting feature. + foreach ($newpluginclasscolumns as $key => $newpluginclasscolumn) { + $questionbankclasscolumns[$key] = $newpluginclasscolumn; + } + + return $questionbankclasscolumns; + } + + /** + * Loads all the available columns. + * + * @return array + * @todo MDL-72004 changes for class renaming. + */ + protected function wanted_columns(): array { + global $CFG; + $this->requiredcolumns = []; + + if (empty($CFG->questionbankcolumns)) { + $questionbankcolumns = $this->get_question_bank_plugins(); + foreach ($questionbankcolumns as $classobject) { + $this->requiredcolumns[get_class($classobject)] = $classobject; + } + } else { + // Config overrides the array, but uses the deprecated classes. + $questionbankcolumns = explode(',', $CFG->questionbankcolumns); + foreach ($questionbankcolumns as $fullname) { + if (! class_exists($fullname)) { + if (class_exists('core_question\\bank\\' . $fullname)) { + $fullname = 'core_question\\bank\\' . $fullname; + } else { + throw new \coding_exception("No such class exists: $fullname"); + } + } + $this->requiredcolumns[$fullname] = new $fullname($this); + } + } + + return $this->requiredcolumns; + } + + + /** + * 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 'question_bank_question_name_column'; + } + + /** + * Initializing table columns + * + * @param array $wanted Collection of column names + * @param string $heading The name of column that is set as heading + * @todo MDL-72004 changes for class renaming. + */ + 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 || $column instanceof \core_question\bank\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[get_class($column)] = $column; + } else { + $this->visiblecolumns[get_class($column)] = $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->init_sort_from_params(); + if (empty($this->sort)) { + $this->sort = $this->default_sort(); + } + } + + /** + * Deal with a sort name of the form columnname, or colname_subsort by + * 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 = ''; + } + // 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); + } + // Validate the subsort, if present. + if ($subsort) { + $subsorts = $column->is_sortable(); + if (!is_array($subsorts) || !isset($subsorts[$subsort])) { + throw new \moodle_exception('unknownsortcolumn', '', $link = $this->baseurl->out(), $sort); + } + } + return [$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; + } + return $params; + } + + /** + * Default sort for question data. + * @return int[] + * @todo MDL-72004 changes for class renaming. + */ + protected function default_sort(): array { + $defaultsort = []; + if (class_exists('\\qbank_viewquestiontype\\question_type_column')) { + $sort = 'qbank_viewquestiontype\question_type_column'; + } else { + $sort = 'core_question\bank\question_type_column'; + } + $defaultsort[$sort] = 1; + if (class_exists('\\qbank_viewquestionname\\question_name_idnumber_tags_column')) { + $sort = 'qbank_viewquestionname\question_name_idnumber_tags_column'; + } else { + $sort = 'core_question\bank\question_name_idnumber_tags_column'; + } + $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. + * @return int the current sort order for this column -1, 0, 1 + */ + public function get_primary_sort_order($sort): int { + $order = reset($this->sort); + $primarysort = key($this->sort); + if ($sort == $primarysort) { + return $order; + } else { + return 0; + } + } + + /** + * Get a URL to redisplay the page with a new sort for the question bank. + * + * @param string $sort the column, or column_subsort to sort on. + * @param bool $newsortreverse whether to sort in reverse order. + * @return string The new URL. + */ + public function new_sort_url($sort, $newsortreverse): string { + if ($newsortreverse) { + $order = -1; + } else { + $order = 1; + } + // Tricky code to add the new sort at the start, removing it from where it was before, if it was present. + $newsort = array_reverse($this->sort); + if (isset($newsort[$sort])) { + unset($newsort[$sort]); + } + $newsort[$sort] = $order; + $newsort = array_reverse($newsort); + if (count($newsort) > self::MAX_SORTS) { + $newsort = array_slice($newsort, 0, self::MAX_SORTS, true); + } + return $this->baseurl->out(true, $this->sort_to_params($newsort)); + } + + /** + * Create the SQL query to retrieve the indicated questions, based on + * \core_question\bank\search\condition filters. + * @todo MDL-72004 changes for class renaming and default sort. + */ + protected function build_query(): void { + // Get the required tables and fields. + $joins = []; + $fields = ['q.hidden', 'q.category']; + if (!empty($this->requiredcolumns)) { + foreach ($this->requiredcolumns as $column) { + $extrajoins = $column->get_extra_joins(); + foreach ($extrajoins as $prefix => $join) { + if (isset($joins[$prefix]) && $joins[$prefix] != $join) { + throw new \coding_exception('Join ' . $join . ' conflicts with previous join ' . $joins[$prefix]); + } + $joins[$prefix] = $join; + } + $fields = array_merge($fields, $column->get_required_fields()); + } + } + $fields = array_unique($fields); + + // Build the order by clause. + $sorts = []; + foreach ($this->sort as $sort => $order) { + list($colname, $subsort) = $this->parse_subsort($sort); + $sorts[] = $this->requiredcolumns[$colname]->sort_expression($order < 0, $subsort); + } + + // Build the where clause. + $tests = ['q.parent = 0']; + $this->sqlparams = []; + foreach ($this->searchconditions as $searchcondition) { + if ($searchcondition->where()) { + $tests[] = '((' . $searchcondition->where() .'))'; + } + if ($searchcondition->params()) { + $this->sqlparams = array_merge($this->sqlparams, $searchcondition->params()); + } + } + // Build the SQL. + $sql = ' FROM {question} q ' . implode(' ', $joins); + $sql .= ' WHERE ' . implode(' AND ', $tests); + $this->countsql = 'SELECT count(1)' . $sql; + $this->loadsql = 'SELECT ' . implode(', ', $fields) . $sql . ' ORDER BY ' . implode(', ', $sorts); + } + + /** + * Get the number of questions. + * @return int + */ + protected function get_question_count(): int { + global $DB; + return $DB->count_records_sql($this->countsql, $this->sqlparams); + } + + /** + * 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 { + global $DB; + $questions = $DB->get_recordset_sql($this->loadsql, $this->sqlparams, $page * $perpage, $perpage); + 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); + } + return $questions; + } + + /** + * Returns the base 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. + */ + public function preview_question_url($questiondata) { + return question_preview_url($questiondata->id, null, null, null, null, + $this->get_most_specific_context()); + } + + /** + * Shows the question bank interface. + * + * The function also processes a number of actions: + * + * Actions affecting the question pool: + * move Moves a question to a different category + * deleteselected Deletes the selected questions from the category + * Other actions: + * category Chooses the category + * params: $tabname question bank edit tab name, for permission checking + * $pagevars current list of page variables + * + * @param string $tabname + * @param array $pagevars + */ + public function display($pagevars, $tabname): void { + + $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']; + } + + echo \html_writer::start_div('questionbankwindow boxwidthwide boxaligncenter'); + + // This one will become redundant after implementing bulk actions plugin. + if ($this->process_actions_needing_ui()) { + return; + } + + $editcontexts = $this->contexts->having_one_edit_tab_cap($tabname); + + // Show the filters and search options. + $this->wanted_filters($cat, $tagids, $showhidden, $recurse, $editcontexts, $showquestiontext); + + // Continues with list of questions. + $this->display_question_list($this->baseurl, $cat, null, $page, $perpage, + $this->contexts->having_cap('moodle/question:add')); + 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); + $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); + } + + /** + * Print the text if category id not available. + */ + protected function print_choose_category_message(): void { + 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 + */ + protected function get_current_category($categoryandcontext) { + 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. + */ + protected function display_options_form($showquestiontext): void { + 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->get_path()), '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 + */ + protected function display_advanced_search_form($advancedsearch): void { + 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. + */ + protected function display_showtext_checkbox($showquestiontext): void { + 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); + } + } else { + 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. + * + * @param false|mixed|\stdClass $category + * @param bool $canadd + */ + protected function create_new_question_form($category, $canadd): void { + echo \html_writer::start_tag('div', ['class' => "createnewquestion"]); + if ($canadd) { + create_new_question_button($category->id, $this->editquestionurl->params(), + get_string('createnewquestion', 'question')); + } else { + print_string('nopermissionadd', 'question'); + } + echo \html_writer::end_tag('div'); + } + + /** + * Prints the table of questions in a category with interactions + * + * @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 $perpage Number of questions to show per page + * @param array $addcontexts contexts where the user is allowed to add new questions. + */ + protected function display_question_list($pageurl, $categoryandcontext, $recurse = 1, $page = 0, + $perpage = 100, $addcontexts = []): void { + global $OUTPUT; + // 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); + + $category = $this->get_current_category($categoryandcontext); + + list($categoryid, $contextid) = explode(',', $categoryandcontext); + $catcontext = \context::instance_by_id($contextid); + + $canadd = has_capability('moodle/question:add', $catcontext); + + $this->create_new_question_form($category, $canadd); + + $this->build_query(); + $totalnumber = $this->get_question_count(); + if ($totalnumber == 0) { + return; + } + $questionsrs = $this->load_page_questions($page, $perpage); + $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); + } + + $pageingurl = new \moodle_url($pageurl->get_path(), $pageurl->params()); + $pagingbar = new \paging_bar($totalnumber, $page, $perpage, $pageingurl); + $pagingbar->pagevar = 'qpage'; + + $this->display_top_pagnation($OUTPUT->render($pagingbar)); + + // This html will be refactored in the bulk actions implementation. + echo \html_writer::start_tag('form', ['action' => $pageurl->get_path(), 'method' => 'post']); + 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); + + $this->display_questions($questions); + + $this->display_bottom_pagination($OUTPUT->render($pagingbar), $totalnumber, $perpage, $pageurl); + + $this->display_bottom_controls($totalnumber, $recurse, $category, $catcontext, $addcontexts); + + echo \html_writer::end_tag('fieldset'); + echo \html_writer::end_tag('form'); + } + + /** + * Display the top pagination bar. + * + * @param object $pagination + */ + protected function display_top_pagnation($pagination): void { + 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 + */ + protected function display_bottom_pagination($pagination, $totalnumber, $perpage, $pageurl): void { + global $PAGE; + $displaydata = array ( + 'extraclasses' => 'pagingbottom', + 'pagination' => $pagination, + 'biggertotal' => true, + ); + if ($totalnumber > DEFAULT_QUESTIONS_PER_PAGE) { + $displaydata['showall'] = true; + if ($perpage == DEFAULT_QUESTIONS_PER_PAGE) { + $url = new \moodle_url($pageurl->get_path(), 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->get_path(), array_merge($pageurl->params(), + ['qperpage' => DEFAULT_QUESTIONS_PER_PAGE])); + $displaydata['totalnumber'] = DEFAULT_QUESTIONS_PER_PAGE; + } + $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 int $totalnumber Total number of questions that might be shown (if it was not for paging). + * @param bool $recurse Whether to include subcategories. + * @param \stdClass $category The question_category row from the database. + * @param \context $catcontext The context of the category being displayed. + * @param array $addcontexts contexts where the user is allowed to add new questions. + */ + protected function display_bottom_controls($totalnumber, $recurse, $category, \context $catcontext, array $addcontexts): void { + $caneditall = has_capability('moodle/question:editall', $catcontext); + $canuseall = has_capability('moodle/question:useall', $catcontext); + $canmoveall = has_capability('moodle/question:moveall', $catcontext); + + echo \html_writer::start_tag('div', ['class' => "modulespecificbuttonscontainer"]); + if ($caneditall || $canmoveall || $canuseall) { + $withselectedcontent = ' ' . get_string('withselected', 'question') . ':'; + echo \html_writer::tag('strong', $withselectedcontent); + echo \html_writer::empty_tag('br'); + + // Print delete and move selected question. + if ($caneditall) { + echo \html_writer::empty_tag('input', [ + 'type' => 'submit', + 'class' => 'btn btn-secondary mr-1', + 'name' => 'deleteselected', + 'value' => get_string('delete'), + 'data-action' => 'toggle', + 'data-togglegroup' => 'qbank', + 'data-toggle' => 'action', + 'disabled' => true, + ]); + } + + if ($canmoveall && count($addcontexts)) { + echo \html_writer::empty_tag('input', [ + 'type' => 'submit', + 'class' => 'btn btn-secondary mr-1', + 'name' => 'move', + 'value' => get_string('moveto', 'question'), + 'data-action' => 'toggle', + 'data-togglegroup' => 'qbank', + 'data-toggle' => 'action', + 'disabled' => true, + ]); + question_category_select_menu($addcontexts, false, 0, "{$category->id},{$category->contextid}"); + } + } + + echo \html_writer::end_tag('div'); + } + + /** + * Display the questions. + * + * @param array $questions + */ + protected function display_questions($questions): void { + echo \html_writer::start_tag('div', + ['class' => 'categoryquestionscontainer', 'id' => 'questionscontainer']); + $this->print_table($questions); + echo \html_writer::end_tag('div'); + } + + /** + * 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']); + + // Prints the table header. + echo \html_writer::start_tag('thead'); + echo \html_writer::start_tag('tr'); + $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. + * + * @deprecated since Moodle 4.0 + * @see print_table() + */ + protected function start_table() { + debugging('Function start_table() is deprecated, please use print_table() instead.', DEBUG_DEVELOPER); + echo '' . "\n"; + echo "\n"; + $this->print_table_headers(); + echo "\n"; + echo "\n"; + } + + /** + * End of the table html. + * + * @deprecated since Moodle 4.0 + * @see print_table() + */ + protected function end_table() { + debugging('Function end_table() is deprecated, please use print_table() instead.', DEBUG_DEVELOPER); + echo "\n"; + echo "
\n"; + } + + /** + * Print table headers from child classes. + */ + protected function print_table_headers(): void { + foreach ($this->visiblecolumns as $column) { + $column->display_header(); + } + } + + /** + * 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->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 { + $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. + * + */ + public function process_actions(): void { + global $DB; + // Now, check for commands on this page and modify variables as necessary. + if (optional_param('move', false, PARAM_BOOL) and confirm_sesskey()) { + // Move selected questions to new category. + $category = required_param('category', PARAM_SEQUENCE); + list($tocategoryid, $contextid) = explode(',', $category); + if (! $tocategory = $DB->get_record('question_categories', + ['id' => $tocategoryid, 'contextid' => $contextid])) { + throw new \moodle_exception('cannotfindcate', 'question'); + } + $tocontext = \context::instance_by_id($contextid); + require_capability('moodle/question:add', $tocontext); + $rawdata = (array) data_submitted(); + $questionids = []; + foreach ($rawdata as $key => $value) { + // Parse input for question ids. + if (preg_match('!^q([0-9]+)$!', $key, $matches)) { + $key = $matches[1]; + $questionids[] = $key; + } + } + if ($questionids) { + list($usql, $params) = $DB->get_in_or_equal($questionids); + $questions = $DB->get_records_sql(" + SELECT q.*, c.contextid + FROM {question} q + JOIN {question_categories} c ON c.id = q.category + WHERE q.id {$usql}", $params); + foreach ($questions as $question) { + question_require_capability_on($question, 'move'); + } + question_move_questions_to_category($questionids, $tocategory->id); + redirect($this->baseurl->out(false, ['category' => "{$tocategoryid},{$contextid}"])); + } + } + + if (optional_param('deleteselected', false, PARAM_BOOL)) { + // Delete selected questions from the category. + // If teacher has already confirmed the action. + if (($confirm = optional_param('confirm', '', PARAM_ALPHANUM)) and confirm_sesskey()) { + $deleteselected = required_param('deleteselected', PARAM_RAW); + if ($confirm == md5($deleteselected)) { + if ($questionlist = explode(',', $deleteselected)) { + // For each question either hide it if it is in use or delete it. + foreach ($questionlist as $questionid) { + $questionid = (int)$questionid; + question_require_capability_on($questionid, 'edit'); + if (questions_in_use([$questionid])) { + $DB->set_field('question', 'hidden', 1, ['id' => $questionid]); + } else { + question_delete_question($questionid); + } + } + } + redirect($this->baseurl); + } else { + throw new \moodle_exception('invalidconfirm', 'question'); + } + } + } + + // Unhide a question. + if (($unhide = optional_param('unhide', '', PARAM_INT)) and confirm_sesskey()) { + question_require_capability_on($unhide, 'edit'); + $DB->set_field('question', 'hidden', 0, ['id' => $unhide]); + + // Purge these questions from the cache. + \question_bank::notify_question_edited($unhide); + + redirect($this->baseurl); + } + } + + /** + * Process actions with ui. + * @return bool + */ + public function process_actions_needing_ui(): bool { + global $DB, $OUTPUT; + if (optional_param('deleteselected', false, PARAM_BOOL)) { + // Make a list of all the questions that are selected. + $rawquestions = $_REQUEST; // This code is called by both POST forms and GET links, so cannot use data_submitted. + $questionlist = ''; // Comma separated list of ids of questions to be deleted. + $questionnames = ''; // String with names of questions separated by
with an asterix + // in front of those that are in use. + $inuse = false; // Set to true if at least one of the questions is in use. + foreach ($rawquestions as $key => $value) { // Parse input for question ids. + if (preg_match('!^q([0-9]+)$!', $key, $matches)) { + $key = $matches[1]; + $questionlist .= $key.','; + question_require_capability_on((int)$key, 'edit'); + if (questions_in_use([$key])) { + $questionnames .= '* '; + $inuse = true; + } + $questionnames .= $DB->get_field('question', 'name', ['id' => $key]) . '
'; + } + } + if (!$questionlist) { // No questions were selected. + redirect($this->baseurl); + } + $questionlist = rtrim($questionlist, ','); + + // Add an explanation about questions in use. + if ($inuse) { + $questionnames .= '
'.get_string('questionsinuse', 'question'); + } + $baseurl = new \moodle_url($this->baseurl->get_path(), $this->baseurl->params()); + $deleteurl = new \moodle_url($baseurl, ['deleteselected' => $questionlist, 'confirm' => md5($questionlist), + 'sesskey' => sesskey()]); + + $continue = new \single_button($deleteurl, get_string('delete'), 'post'); + echo $OUTPUT->confirm(get_string('deletequestionscheck', 'question', $questionnames), $continue, $baseurl); + + return true; + } + + return false; + } + + /** + * Add another search control to this view. + * @param condition $searchcondition the condition to add. + */ + public function add_searchcondition($searchcondition): void { + $this->searchconditions[] = $searchcondition; + } + +} diff --git a/question/edit.php b/question/edit.php index 8c6e069c2a9..a5444594f95 100644 --- a/question/edit.php +++ b/question/edit.php @@ -36,7 +36,9 @@ if (($lastchanged = optional_param('lastchanged', 0, PARAM_INT)) !== 0) { } $PAGE->set_url($url); -$questionbank = new core_question\bank\view($contexts, $thispageurl, $COURSE, $cm); +$questionbank = new core_question\local\bank\view($contexts, $thispageurl, $COURSE, $cm); + +// TODO MDL-72076 - this one will become redundant after implementing bulk actions UI. $questionbank->process_actions(); $context = $contexts->lowest(); @@ -49,11 +51,8 @@ echo $OUTPUT->header(); $renderer = $PAGE->get_renderer('core_question', 'bank'); echo $renderer->extra_horizontal_navigation(); -echo '
'; -$questionbank->display('questions', $pagevars['qpage'], $pagevars['qperpage'], - $pagevars['cat'], $pagevars['recurse'], $pagevars['showhidden'], - $pagevars['qbshowtext'], $pagevars['qtagids']); -echo "
\n"; +// Print the question area. +$questionbank->display($pagevars, 'questions'); // Log the view of this category. list($categoryid, $contextid) = explode(',', $pagevars['cat']); diff --git a/question/editlib.php b/question/editlib.php index f1360717795..8da8963cb4f 100644 --- a/question/editlib.php +++ b/question/editlib.php @@ -140,13 +140,14 @@ function question_can_delete_cat($todelete) { /** - * Base class for representing a column in a {@link question_bank_view}. + * Base class for representing a column in a {@see question_bank_view}. * * @copyright 2009 Tim Hunt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @deprecated since Moodle 2.7 MDL-40457 + * @todo MDl-72004 delete the class alias, not done in MDL-71516 for any potential error from other plugins. */ -class_alias('core_question\bank\column_base', 'question_bank_column_base', true); +class_alias('core_question\local\bank\column_base', 'question_bank_column_base', true); /** * A column with a checkbox for each question with name q{questionid}. @@ -154,8 +155,9 @@ class_alias('core_question\bank\column_base', 'question_bank_column_base', true) * @copyright 2009 Tim Hunt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @deprecated since Moodle 2.7 MDL-40457 + * @todo MDl-72004 delete the class alias, not done in MDL-71516 for any potential error from other plugins. */ -class_alias('core_question\bank\checkbox_column', 'question_bank_checkbox_column', true); +class_alias('core_question\local\bank\checkbox_column', 'question_bank_checkbox_column', true); /** * A column type for the name of the question type. @@ -163,6 +165,7 @@ class_alias('core_question\bank\checkbox_column', 'question_bank_checkbox_column * @copyright 2009 Tim Hunt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @deprecated since Moodle 2.7 MDL-40457 + * @todo MDl-72004 delete the class alias, not done in MDL-71516 for any potential error from other plugins. */ class_alias('core_question\bank\question_type_column', 'question_bank_question_type_column', true); @@ -173,6 +176,7 @@ class_alias('core_question\bank\question_type_column', 'question_bank_question_t * @copyright 2009 Tim Hunt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @deprecated since Moodle 2.7 MDL-40457 + * @todo MDl-72004 delete the class alias, not done in MDL-71516 for any potential error from other plugins. */ class_alias('core_question\bank\question_name_column', 'question_bank_question_name_column', true); @@ -183,6 +187,7 @@ class_alias('core_question\bank\question_name_column', 'question_bank_question_n * @copyright 2009 Tim Hunt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @deprecated since Moodle 2.7 MDL-40457 + * @todo MDl-72004 delete the class alias, not done in MDL-71516 for any potential error from other plugins. */ class_alias('core_question\bank\creator_name_column', 'question_bank_creator_name_column', true); @@ -193,6 +198,7 @@ class_alias('core_question\bank\creator_name_column', 'question_bank_creator_nam * @copyright 2009 Tim Hunt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @deprecated since Moodle 2.7 MDL-40457 + * @todo MDl-72004 delete the class alias, not done in MDL-71516 for any potential error from other plugins. */ class_alias('core_question\bank\modifier_name_column', 'question_bank_modifier_name_column', true); @@ -203,8 +209,9 @@ class_alias('core_question\bank\modifier_name_column', 'question_bank_modifier_n * @copyright 2009 Tim Hunt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @deprecated since Moodle 2.7 MDL-40457 + * @todo MDl-72004 delete the class alias, not done in MDL-71516 for any potential error from other plugins. */ -class_alias('core_question\bank\action_column_base', 'question_bank_action_column_base', true); +class_alias('core_question\local\bank\action_column_base', 'question_bank_action_column_base', true); /** @@ -213,6 +220,7 @@ class_alias('core_question\bank\action_column_base', 'question_bank_action_colum * @copyright 2009 Tim Hunt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @deprecated since Moodle 2.7 MDL-40457 + * @todo MDl-72004 delete the class alias, not done in MDL-71516 for any potential error from other plugins. */ class_alias('core_question\bank\edit_action_column', 'question_bank_edit_action_column', true); @@ -222,6 +230,7 @@ class_alias('core_question\bank\edit_action_column', 'question_bank_edit_action_ * @copyright 2013 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @deprecated since Moodle 2.7 MDL-40457 + * @todo MDl-72004 delete the class alias, not done in MDL-71516 for any potential error from other plugins. */ class_alias('core_question\bank\copy_action_column', 'question_bank_copy_action_column', true); @@ -231,6 +240,7 @@ class_alias('core_question\bank\copy_action_column', 'question_bank_copy_action_ * @copyright 2009 Tim Hunt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @deprecated since Moodle 2.7 MDL-40457 + * @todo MDl-72004 delete the class alias, not done in MDL-71516 for any potential error from other plugins. */ class_alias('core_question\bank\preview_action_column', 'question_bank_preview_action_column', true); @@ -241,6 +251,7 @@ class_alias('core_question\bank\preview_action_column', 'question_bank_preview_a * @copyright 2009 Tim Hunt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @deprecated since Moodle 2.7 MDL-40457 + * @todo MDl-72004 delete the class alias, not done in MDL-71516 for any potential error from other plugins. */ class_alias('core_question\bank\delete_action_column', 'question_bank_delete_action_column', true); @@ -250,8 +261,9 @@ class_alias('core_question\bank\delete_action_column', 'question_bank_delete_act * @copyright 2009 Tim Hunt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @deprecated since Moodle 2.7 MDL-40457 + * @todo MDl-72004 delete the class alias, not done in MDL-71516 for any potential error from other plugins. */ -class_alias('core_question\bank\row_base', 'question_bank_row_base', true); +class_alias('core_question\local\bank\row_base', 'question_bank_row_base', true); /** * A column type for the name of the question name. @@ -259,6 +271,7 @@ class_alias('core_question\bank\row_base', 'question_bank_row_base', true); * @copyright 2009 Tim Hunt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @deprecated since Moodle 2.7 MDL-40457 + * @todo MDl-72004 delete the class alias, not done in MDL-71516 for any potential error from other plugins. */ class_alias('core_question\bank\question_text_row', 'question_bank_question_text_row', true); @@ -266,8 +279,9 @@ class_alias('core_question\bank\question_text_row', 'question_bank_question_text * @copyright 2009 Tim Hunt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @deprecated since Moodle 2.7 MDL-40457 + * @todo MDl-72004 delete the class alias, not done in MDL-71516 for any potential error from other plugins. */ -class_alias('core_question\bank\view', 'question_bank_view', true); +class_alias('core_question\local\bank\view', 'question_bank_view', true); /** * Common setup for all pages for editing questions. diff --git a/question/preview.php b/question/preview.php index 52b455a6c93..899da99c711 100644 --- a/question/preview.php +++ b/question/preview.php @@ -87,7 +87,7 @@ if ($previewid) { // actually from the user point of view, it makes sense. print_error('submissionoutofsequencefriendlymessage', 'question', question_preview_url($question->id, $options->behaviour, - $options->maxmark, $options, $options->variant, $context), null, $e); + $options->maxmark, $options, $options->variant, $context), null, $e); } if ($quba->get_owning_context()->instanceid != $USER->id) { @@ -255,15 +255,15 @@ echo $quba->render_question($slot, $options, $displaynumber); // Finish the question form. echo html_writer::start_tag('div', array('id' => 'previewcontrols', 'class' => 'controls')); echo html_writer::empty_tag('input', $restartdisabled + array('type' => 'submit', - 'name' => 'restart', 'value' => get_string('restart', 'question'), 'class' => 'btn btn-secondary')); + 'name' => 'restart', 'value' => get_string('restart', 'question'), 'class' => 'btn btn-secondary')); echo html_writer::empty_tag('input', $finishdisabled + array('type' => 'submit', - 'name' => 'save', 'value' => get_string('save', 'question'), 'class' => 'btn btn-secondary', - 'id' => 'id_save_question_preview')); + 'name' => 'save', 'value' => get_string('save', 'question'), 'class' => 'btn btn-secondary', + 'id' => 'id_save_question_preview')); echo html_writer::empty_tag('input', $filldisabled + array('type' => 'submit', - 'name' => 'fill', 'value' => get_string('fillincorrect', 'question'), 'class' => 'btn btn-secondary')); + 'name' => 'fill', 'value' => get_string('fillincorrect', 'question'), 'class' => 'btn btn-secondary')); echo html_writer::empty_tag('input', $finishdisabled + array('type' => 'submit', - 'name' => 'finish', 'value' => get_string('submitandfinish', 'question'), 'class' => 'btn btn-secondary', - 'id' => 'id_finish_question_preview')); + 'name' => 'finish', 'value' => get_string('submitandfinish', 'question'), 'class' => 'btn btn-secondary', + 'id' => 'id_finish_question_preview')); echo html_writer::end_tag('div'); echo html_writer::end_tag('form'); @@ -277,8 +277,17 @@ print_collapsible_region_end(); // Output a link to export this single question. if (question_has_capability_on($question, 'view')) { - echo html_writer::link(question_get_export_single_question_url($question), - get_string('exportonequestion', 'question')); + if (class_exists('qbank_exporttoxml\\exporttoxml_helper')) { + if (\core\plugininfo\qbank::is_plugin_enabled('qbank_exporttoxml')) { + $exportfunction = '\\qbank_exporttoxml\\exporttoxml_helper::question_get_export_single_question_url'; + echo html_writer::link($exportfunction($question), + get_string('exportonequestion', 'question')); + } + } else { + $exportfunction = 'question_get_export_single_question_url'; + echo html_writer::link($exportfunction($question), + get_string('exportonequestion', 'question')); + } } // Log the preview of this question. @@ -290,10 +299,9 @@ $optionsform->display(); $PAGE->requires->js_module('core_question_engine'); $PAGE->requires->strings_for_js(array( - 'closepreview', + 'closepreview', ), 'question'); $PAGE->requires->yui_module('moodle-question-preview', 'M.question.preview.init'); $PAGE->requires->js_call_amd('core_form/submit', 'init', ['id_save_question_preview']); $PAGE->requires->js_call_amd('core_form/submit', 'init', ['id_finish_question_preview']); echo $OUTPUT->footer(); - diff --git a/question/renderer.php b/question/renderer.php index 76ef282cda5..e08831536ab 100644 --- a/question/renderer.php +++ b/question/renderer.php @@ -17,8 +17,7 @@ /** * Renderers for outputting parts of the question bank. * - * @package moodlecore - * @subpackage questionbank + * @package core_question * @copyright 2011 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -38,9 +37,10 @@ class core_question_bank_renderer extends plugin_renderer_base { /** * Display additional navigation if needed. * + * @param string $active * @return string */ - public function extra_horizontal_navigation() { + public function extra_horizontal_navigation($active = null) { // Horizontal navigation for question bank. if ($questionnode = $this->page->settingsnav->find("questionbank", \navigation_node::TYPE_CONTAINER)) { if ($children = $questionnode->children) { @@ -48,8 +48,11 @@ class core_question_bank_renderer extends plugin_renderer_base { foreach ($children as $key => $node) { $tabs[] = new \tabobject($node->key, $node->action, $node->text); } - $active = $questionnode->find_active_node()->key; - return \html_writer::div(print_tabs([$tabs], $active, null, null, true), 'questionbank-navigation'); + if (empty($active)) { + $active = $questionnode->find_active_node()->key; + } + return \html_writer::div(print_tabs([$tabs], $active, null, null, true), + 'questionbank-navigation'); } } return ''; @@ -68,6 +71,26 @@ class core_question_bank_renderer extends plugin_renderer_base { return $this->image_icon('icon', $namestr, $qtype->plugin_name(), array('title' => $namestr)); } + /** + * Render the column headers. + * + * @param array $qbankheaderdata + * @return bool|string + */ + public function render_column_header($qbankheaderdata) { + return $this->render_from_template('core_question/column_header', $qbankheaderdata); + } + + /** + * Render the column sort elements. + * + * @param array $sortdata + * @return bool|string + */ + public function render_column_sort($sortdata) { + return $this->render_from_template('core_question/column_sort', $sortdata); + } + /** * Render a qbank_chooser. * @@ -78,6 +101,56 @@ class core_question_bank_renderer extends plugin_renderer_base { return $this->render_from_template('core_question/qbank_chooser', $qbankchooser->export_for_template($this)); } + /** + * Render category condition. + * + * @param array $displaydata + * @return bool|string + */ + public function render_category_condition($displaydata) { + return $this->render_from_template('core_question/category_condition', $displaydata); + } + + /** + * Render category condition advanced. + * + * @param array $displaydata + * @return bool|string + */ + public function render_category_condition_advanced($displaydata) { + return $this->render_from_template('core_question/category_condition_advanced', $displaydata); + } + + /** + * Render hidden condition advanced. + * + * @param array $displaydata + * @return bool|string + */ + public function render_hidden_condition_advanced($displaydata) { + return $this->render_from_template('core_question/hidden_condition_advanced', $displaydata); + } + + /** + * Render question pagination. + * + * @param array $displaydata + * @return bool|string + */ + public function render_question_pagination($displaydata) { + return $this->render_from_template('core_question/question_pagination', $displaydata); + } + + /** + * Render question showtext checkbox. + * + * @param array $displaydata + * @return bool|string + */ + public function render_showtext_checkbox($displaydata) { + return $this->render_from_template('core_question/showtext_checkbox', $displaydata); + } + /** * Build the HTML for the question chooser javascript popup. * @@ -89,7 +162,7 @@ class core_question_bank_renderer extends plugin_renderer_base { */ public function qbank_chooser($real, $fake, $course, $hiddenparams) { debugging('Method core_question_bank_renderer::qbank_chooser() is deprecated, ' . - 'see core_question_bank_renderer::render_qbank_chooser().', DEBUG_DEVELOPER); + 'see core_question_bank_renderer::render_qbank_chooser().', DEBUG_DEVELOPER); return ''; } @@ -101,7 +174,7 @@ class core_question_bank_renderer extends plugin_renderer_base { */ protected function qbank_chooser_types($types) { debugging('Method core_question_bank_renderer::qbank_chooser_types() is deprecated, ' . - 'see core_question_bank_renderer::render_qbank_chooser().', DEBUG_DEVELOPER); + 'see core_question_bank_renderer::render_qbank_chooser().', DEBUG_DEVELOPER); return ''; } @@ -115,7 +188,7 @@ class core_question_bank_renderer extends plugin_renderer_base { */ protected function qbank_chooser_qtype($qtype, $classes = array()) { debugging('Method core_question_bank_renderer::qbank_chooser_qtype() is deprecated, ' . - 'see core_question_bank_renderer::render_qbank_chooser().', DEBUG_DEVELOPER); + 'see core_question_bank_renderer::render_qbank_chooser().', DEBUG_DEVELOPER); return ''; } @@ -128,7 +201,8 @@ class core_question_bank_renderer extends plugin_renderer_base { */ protected function qbank_chooser_title($title, $identifier = null) { debugging('Method core_question_bank_renderer::qbank_chooser_title() is deprecated, ' . - 'see core_question_bank_renderer::render_qbank_chooser().', DEBUG_DEVELOPER); + 'see core_question_bank_renderer::render_qbank_chooser().', DEBUG_DEVELOPER); return ''; } + } diff --git a/question/templates/category_condition.mustache b/question/templates/category_condition.mustache new file mode 100644 index 00000000000..90cbc27d59a --- /dev/null +++ b/question/templates/category_condition.mustache @@ -0,0 +1,38 @@ +{{! + 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 . +}} +{{! + @template core_question/category_condition + + Example context (json): + { + "displaydata": [ + { + "categoryselect": "html string", + "categorydesc": "html string" + } + ] + } +}} +
+ + {{{categoryselect}}} +
+
+ {{{categorydesc}}} +
diff --git a/question/templates/category_condition_advanced.mustache b/question/templates/category_condition_advanced.mustache new file mode 100644 index 00000000000..ec5ac2f8042 --- /dev/null +++ b/question/templates/category_condition_advanced.mustache @@ -0,0 +1,33 @@ +{{! + 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 . +}} +{{! + @template core_question/category_condition_advanced + + Example context (json): + { + "displaydata": [ + { + "checked": "checked attribute" + } + ] + } +}} +
+ + + +
diff --git a/question/templates/column_header.mustache b/question/templates/column_header.mustache new file mode 100644 index 00000000000..43d231374df --- /dev/null +++ b/question/templates/column_header.mustache @@ -0,0 +1,52 @@ +{{! + 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 . +}} +{{! + @template core_question/column_header + + Example context (json): + { + "qbankheaderdata": [ + { + "extraclasses": "checkbox", + "sortable": false, + "tiptitle": "Element title", + "sorttip": true, + "tip": "Select questions for bulk actions", + "sortlinks": "sort / sort" + } + ] + } +}} + + {{#title}} +
{{title}}
+ {{/title}} + {{^sortable}} + {{^sorttip}} + {{tiptitle}} + {{/sorttip}} + {{#sorttip}} + {{{tiptitle}}} + {{/sorttip}} + {{/sortable}} + {{#sortable}} +
+ {{{sortlinks}}} +
+ {{/sortable}} + + diff --git a/question/templates/column_sort.mustache b/question/templates/column_sort.mustache new file mode 100644 index 00000000000..7d13f0cc372 --- /dev/null +++ b/question/templates/column_sort.mustache @@ -0,0 +1,34 @@ +{{! + 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 . +}} +{{! + @template core_question/column_sort + + Example context (json): + { + "sortdata": [ + { + "sorturl": "url", + "sorttip": "Sort according to first name", + "sortcontent": "Sort title sort icon" + } + ] + } +}} + + {{{sortcontent}}} + + diff --git a/question/templates/hidden_condition_advanced.mustache b/question/templates/hidden_condition_advanced.mustache new file mode 100644 index 00000000000..cea0a0ced77 --- /dev/null +++ b/question/templates/hidden_condition_advanced.mustache @@ -0,0 +1,33 @@ +{{! + 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 . +}} +{{! + @template core_question/hidden_condition_advanced + + Example context (json): + { + "displaydata": [ + { + "checked": "checked attribute" + } + ] + } +}} +
+ + + +
diff --git a/question/templates/question_pagination.mustache b/question/templates/question_pagination.mustache new file mode 100644 index 00000000000..9ad7dd4f9da --- /dev/null +++ b/question/templates/question_pagination.mustache @@ -0,0 +1,48 @@ +{{! + 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 . +}} +{{! + @template core_question/question_pagination + + Example context (json): + { + "sortdata": [ + { + "extraclasses": "classes", + "pagination": "pagination html", + "showall": "true/false", + "showallurl": "moodle url", + "biggertotal": "true/false", + "totalnumber": "number of questions" + } + ] + } +}} +
+ {{{pagination}}} +
+{{#showall}} + +{{/showall}} diff --git a/question/templates/showtext_checkbox.mustache b/question/templates/showtext_checkbox.mustache new file mode 100644 index 00000000000..84610f3411c --- /dev/null +++ b/question/templates/showtext_checkbox.mustache @@ -0,0 +1,33 @@ +{{! + 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 . +}} +{{! + @template core_question/showtext_checkbox + + Example context (json): + { + "sortdata": [ + { + "checked": "true/false" + } + ] + } +}} +
+ + + +
diff --git a/question/tests/backup_test.php b/question/tests/backup_test.php index 91cafae780c..f5836094241 100644 --- a/question/tests/backup_test.php +++ b/question/tests/backup_test.php @@ -107,16 +107,16 @@ class core_question_backup_testcase extends advanced_testcase { // Create a new course category and and a new course in that. $category1 = $this->getDataGenerator()->create_category(); - $course = $this->getDataGenerator()->create_course(array('category' => $category1->id)); + $course = $this->getDataGenerator()->create_course(['category' => $category1->id]); $courseshortname = $course->shortname; $coursefullname = $course->fullname; // Create 2 questions. $qgen = $this->getDataGenerator()->get_plugin_generator('core_question'); $context = context_coursecat::instance($category1->id); - $qcat = $qgen->create_question_category(array('contextid' => $context->id)); - $question1 = $qgen->create_question('shortanswer', null, array('category' => $qcat->id, 'idnumber' => 'q1')); - $question2 = $qgen->create_question('shortanswer', null, array('category' => $qcat->id, 'idnumber' => 'q2')); + $qcat = $qgen->create_question_category(['contextid' => $context->id]); + $question1 = $qgen->create_question('shortanswer', null, ['category' => $qcat->id, 'idnumber' => 'q1']); + $question2 = $qgen->create_question('shortanswer', null, ['category' => $qcat->id, 'idnumber' => 'q2']); // Tag the questions with 2 question tags and 2 course level question tags. $qcontext = context::instance_by_id($qcat->contextid); @@ -127,7 +127,7 @@ class core_question_backup_testcase extends advanced_testcase { core_tag_tag::set_item_tags('core_question', 'question', $question2->id, $coursecontext, ['ctag3', 'ctag4']); // Create a quiz and add one of the questions to that. - $quiz = $this->getDataGenerator()->create_module('quiz', array('course' => $course->id)); + $quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id]); quiz_add_quiz_question($question1->id, $quiz); // Backup the course twice for future use. @@ -144,7 +144,7 @@ class core_question_backup_testcase extends advanced_testcase { // The questions should remain in the question category they were which is // a question category belonging to a course category context. - $questions = $DB->get_records('question', array('category' => $qcat->id), 'idnumber'); + $questions = $DB->get_records('question', ['category' => $qcat->id], 'idnumber'); $this->assertCount(2, $questions); // Retrieve tags for each question and check if they are assigned at the right context. @@ -179,10 +179,10 @@ class core_question_backup_testcase extends advanced_testcase { // Create a new course category to restore the backup file into it. $category2 = $this->getDataGenerator()->create_category(); - $expectedwarnings = array( + $expectedwarnings = [ get_string('qcategory2coursefallback', 'backup', (object) ['name' => 'top']), get_string('qcategory2coursefallback', 'backup', (object) ['name' => $qcat->name]) - ); + ]; // Restore to a new course in the new course category. $courseid3 = $this->restore_course($backupid2, $coursefullname, $courseshortname . '_3', $category2->id, $expectedwarnings); @@ -192,7 +192,7 @@ class core_question_backup_testcase extends advanced_testcase { $questions = $DB->get_records_sql("SELECT q.* FROM {question} q JOIN {question_categories} qc ON q.category = qc.id - WHERE qc.contextid = ?", array($coursecontext3->id)); + WHERE qc.contextid = ?", [$coursecontext3->id]); $this->assertCount(2, $questions); // Now, retrieve tags for each question and check if they are assigned at the right context. diff --git a/question/tests/bank_view_test.php b/question/tests/bank_view_test.php index 9108d705005..c76b285ea26 100644 --- a/question/tests/bank_view_test.php +++ b/question/tests/bank_view_test.php @@ -59,9 +59,18 @@ class core_question_bank_view_testcase extends advanced_testcase { $cache->delete($questiondata->id); // Generate the view. - $view = new core_question\bank\view($contexts, new moodle_url('/'), $course); + $view = new core_question\local\bank\view($contexts, new moodle_url('/'), $course); ob_start(); - $view->display('editq', 0, 20, $cat->id . ',' . $context->id, false, false, false); + $pagevars = [ + 'qpage' => 0, + 'qperpage' => 20, + 'cat' => $cat->id . ',' . $context->id, + 'recurse' => false, + 'showhidden' => false, + 'qbshowtext' => false + + ]; + $view->display($pagevars, 'editq'); $html = ob_get_clean(); // Verify the output includes the expected question. @@ -91,9 +100,18 @@ class core_question_bank_view_testcase extends advanced_testcase { $DB->set_field('question', 'qtype', 'unknownqtype', ['id' => $questiondata->id]); // Generate the view. - $view = new core_question\bank\view($contexts, new moodle_url('/'), $course); + $view = new core_question\local\bank\view($contexts, new moodle_url('/'), $course); ob_start(); - $view->display('editq', 0, 20, $cat->id . ',' . $context->id, false, false, false); + $pagevars = [ + 'qpage' => 0, + 'qperpage' => 20, + 'cat' => $cat->id . ',' . $context->id, + 'recurse' => false, + 'showhidden' => false, + 'qbshowtext' => false + + ]; + $view->display($pagevars, 'editq'); $html = ob_get_clean(); // Mainly we are verifying that there was no fatal error. diff --git a/question/tests/behat/behat_question_base.php b/question/tests/behat/behat_question_base.php index 55ab4526f6b..ef3ba202455 100644 --- a/question/tests/behat/behat_question_base.php +++ b/question/tests/behat/behat_question_base.php @@ -50,8 +50,8 @@ class behat_question_base extends behat_base { */ protected function finish_adding_question($questiontypename, TableNode $questiondata) { - $this->execute('behat_forms::i_set_the_field_to', array($this->escape($questiontypename), 1)); - $this->execute("behat_general::i_click_on", array('.submitbutton', "css_element")); + $this->execute('behat_forms::i_set_the_field_to', [$this->escape($questiontypename), 1]); + $this->execute("behat_general::i_click_on", ['.submitbutton', "css_element"]); $this->execute("behat_forms::i_set_the_following_fields_to_these_values", $questiondata); $this->execute("behat_forms::press_button", 'id_submitbutton'); diff --git a/question/tests/events_test.php b/question/tests/events_test.php index bc03892bad5..fda6de4fb4d 100644 --- a/question/tests/events_test.php +++ b/question/tests/events_test.php @@ -44,16 +44,16 @@ class core_question_events_testcase extends advanced_testcase { public function test_question_category_created() { $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); - $quiz = $this->getDataGenerator()->create_module('quiz', array('course' => $course->id)); + $quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course->id]); $contexts = new question_edit_contexts(context_module::instance($quiz->cmid)); - $defaultcategoryobj = question_make_default_categories(array($contexts->lowest())); + $defaultcategoryobj = question_make_default_categories([$contexts->lowest()]); $defaultcategory = $defaultcategoryobj->id . ',' . $defaultcategoryobj->contextid; $qcobject = new question_category_object( 1, - new moodle_url('/mod/quiz/edit.php', array('cmid' => $quiz->cmid)), + new moodle_url('/mod/quiz/edit.php', ['cmid' => $quiz->cmid]), $contexts->having_one_edit_tab_cap('categories'), $defaultcategoryobj->id, $defaultcategory, @@ -69,7 +69,7 @@ class core_question_events_testcase extends advanced_testcase { // Check that the event data is valid. $this->assertInstanceOf('\core\event\question_category_created', $event); $this->assertEquals(context_module::instance($quiz->cmid), $event->get_context()); - $expected = array($course->id, 'quiz', 'addcategory', 'view.php?id=' . $quiz->cmid , $categoryid, $quiz->cmid); + $expected = [$course->id, 'quiz', 'addcategory', 'view.php?id=' . $quiz->cmid , $categoryid, $quiz->cmid]; $this->assertEventLegacyLogData($expected, $event); $this->assertEventContextNotUsed($event); } diff --git a/question/tests/externallib_test.php b/question/tests/externallib_test.php index 2315ae2afd6..06783ce26de 100644 --- a/question/tests/externallib_test.php +++ b/question/tests/externallib_test.php @@ -57,7 +57,7 @@ class core_question_external_testcase extends externallib_advanced_testcase { $this->student = self::getDataGenerator()->create_user(); // Users enrolments. - $this->studentrole = $DB->get_record('role', array('shortname' => 'student')); + $this->studentrole = $DB->get_record('role', ['shortname' => 'student']); $this->getDataGenerator()->enrol_user($this->student->id, $this->course->id, $this->studentrole->id, 'manual'); } @@ -73,7 +73,7 @@ class core_question_external_testcase extends externallib_advanced_testcase { $quba = question_engine::make_questions_usage_by_activity('core_question_update_flag', context_system::instance()); $quba->set_preferred_behaviour('deferredfeedback'); - $questiondata = $questiongenerator->create_question('numerical', null, array('category' => $cat->id)); + $questiondata = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]); $question = question_bank::load_question($questiondata->id); $slot = $quba->add_question($question); $qa = $quba->get_question_attempt($slot); diff --git a/question/tests/fixtures/testable_core_question_column.php b/question/tests/fixtures/testable_core_question_column.php index d467124479d..02dfadb873a 100644 --- a/question/tests/fixtures/testable_core_question_column.php +++ b/question/tests/fixtures/testable_core_question_column.php @@ -31,7 +31,7 @@ defined('MOODLE_INTERNAL') || die(); * @copyright 2018 Huong Nguyen * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class testable_core_question_column extends \core_question\bank\column_base { +class testable_core_question_column extends \core_question\local\bank\column_base { /** @var array sortable columns. */ private $sortable = []; diff --git a/question/tests/generator/lib.php b/question/tests/generator/lib.php index 55dcac6ab6b..f57d1f22e79 100644 --- a/question/tests/generator/lib.php +++ b/question/tests/generator/lib.php @@ -45,14 +45,14 @@ class core_question_generator extends component_generator_base { $this->categorycount++; - $defaults = array( + $defaults = [ 'name' => 'Test question category ' . $this->categorycount, 'info' => '', 'infoformat' => FORMAT_HTML, 'stamp' => make_unique_id_code(), 'sortorder' => 999, 'idnumber' => null - ); + ]; $record = $this->datagenerator->combine_defaults_and_record($defaults, $record); @@ -175,12 +175,12 @@ class core_question_generator extends component_generator_base { $qcat = $this->create_question_category(['contextid' => $context->id]); - $questions = array( + $questions = [ $this->create_question('shortanswer', null, ['category' => $qcat->id]), $this->create_question('shortanswer', null, ['category' => $qcat->id]), - ); + ]; - return array($category, $course, $qcat, $questions); + return [$category, $course, $qcat, $questions]; } /** diff --git a/question/tests/generator_test.php b/question/tests/generator_test.php index 945c9839aa8..39ce4ac3aa1 100644 --- a/question/tests/generator_test.php +++ b/question/tests/generator_test.php @@ -46,8 +46,7 @@ class core_question_generator_testcase extends advanced_testcase { // creates a Top category as well. $this->assertEquals($count, $DB->count_records('question_categories')); - $cat = $generator->create_question_category(array( - 'name' => 'My category', 'sortorder' => 1)); + $cat = $generator->create_question_category(['name' => 'My category', 'sortorder' => 1]); $this->assertSame('My category', $cat->name); $this->assertSame(1, $cat->sortorder); } @@ -61,8 +60,8 @@ class core_question_generator_testcase extends advanced_testcase { $this->assertNull($questions[0]->idnumber); $this->assertNull($questions[1]->idnumber); // Check created idnumbers. - $qcat1 = $generator->create_question_category(array( - 'name' => 'My category', 'sortorder' => 1, 'idnumber' => 'myqcat')); + $qcat1 = $generator->create_question_category([ + 'name' => 'My category', 'sortorder' => 1, 'idnumber' => 'myqcat']); $this->assertSame('myqcat', $qcat1->idnumber); $quest1 = $generator->update_question($questions[0], null, ['idnumber' => 'myquest']); $this->assertSame('myquest', $quest1->idnumber); diff --git a/question/tests/importexport_test.php b/question/tests/importexport_test.php index 96d0704782b..b0b0b2725f6 100644 --- a/question/tests/importexport_test.php +++ b/question/tests/importexport_test.php @@ -57,14 +57,14 @@ class testable_qformat extends qformat_default { class qformat_default_test extends advanced_testcase { public function test_assemble_category_path() { $format = new testable_qformat(); - $pathsections = array( + $pathsections = [ '$course$', "Tim's questions", "Tricky things like / // and so on", 'Category name ending in /', '/ and one that starts with one', 'Matematically Matematiskt (svenska)' - ); + ]; $this->assertEquals('$course$/Tim\'s questions/Tricky things like // //// and so on/Category name ending in // / // and one that starts with one/Matematically Matematiskt (svenska)', $format->assemble_category_path($pathsections)); } @@ -72,20 +72,20 @@ class qformat_default_test extends advanced_testcase { public function test_split_category_path() { $format = new testable_qformat(); $path = '$course$/Tim\'s questions/Tricky things like // //// and so on/Category name ending in // / // and one that starts with one/Matematically Matematiskt (svenska)'; - $this->assertEquals(array( + $this->assertEquals([ '$course$', "Tim's questions", "Tricky things like / // and so on", 'Category name ending in /', '/ and one that starts with one', 'Matematically Matematiskt (svenska)' - ), $format->split_category_path($path)); + ], $format->split_category_path($path)); } public function test_split_category_path_cleans() { $format = new testable_qformat(); $path = 'Nasty thing'; - $this->assertEquals(array('Nasty thing'), $format->split_category_path($path)); + $this->assertEquals(['Nasty thing'], $format->split_category_path($path)); } public function test_clean_question_name() { diff --git a/question/tests/least_used_variant_strategy_test.php b/question/tests/least_used_variant_strategy_test.php index f1b626781ef..d86a521ec61 100644 --- a/question/tests/least_used_variant_strategy_test.php +++ b/question/tests/least_used_variant_strategy_test.php @@ -42,7 +42,7 @@ class least_used_variant_strategy_testcase extends advanced_testcase { $quba->set_preferred_behaviour('deferredfeedback'); $slot = $quba->add_question($question); $quba->start_all_questions(new core_question\engine\variants\least_used_strategy( - $quba, new qubaid_list(array()))); + $quba, new qubaid_list([]))); $this->assertEquals(1, $quba->get_variant($slot)); } @@ -54,7 +54,7 @@ class least_used_variant_strategy_testcase extends advanced_testcase { $slot1 = $quba->add_question($question); $slot2 = $quba->add_question($question); $quba->start_all_questions(new core_question\engine\variants\least_used_strategy( - $quba, new qubaid_list(array()))); + $quba, new qubaid_list([]))); $this->assertEquals($quba->get_variant($slot1), $quba->get_variant($slot2)); } @@ -64,7 +64,7 @@ class least_used_variant_strategy_testcase extends advanced_testcase { $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); $cat = $generator->create_question_category(); - $questiondata = $generator->create_question('calculated', null, array('category' => $cat->id)); + $questiondata = $generator->create_question('calculated', null, ['category' => $cat->id]); // Create two dataset items. $adefinitionid = $DB->get_field_sql(" @@ -72,23 +72,23 @@ class least_used_variant_strategy_testcase extends advanced_testcase { FROM {question_dataset_definitions} qdd JOIN {question_datasets} qd ON qd.datasetdefinition = qdd.id WHERE qd.question = ? - AND qdd.name = ?", array($questiondata->id, 'a')); + AND qdd.name = ?", [$questiondata->id, 'a']); $bdefinitionid = $DB->get_field_sql(" SELECT qdd.id FROM {question_dataset_definitions} qdd JOIN {question_datasets} qd ON qd.datasetdefinition = qdd.id WHERE qd.question = ? - AND qdd.name = ?", array($questiondata->id, 'b')); - $DB->set_field('question_dataset_definitions', 'itemcount', 2, array('id' => $adefinitionid)); - $DB->set_field('question_dataset_definitions', 'itemcount', 2, array('id' => $bdefinitionid)); - $DB->insert_record('question_dataset_items', array('definition' => $adefinitionid, - 'itemnumber' => 1, 'value' => 3)); - $DB->insert_record('question_dataset_items', array('definition' => $bdefinitionid, - 'itemnumber' => 1, 'value' => 7)); - $DB->insert_record('question_dataset_items', array('definition' => $adefinitionid, - 'itemnumber' => 2, 'value' => 6)); - $DB->insert_record('question_dataset_items', array('definition' => $bdefinitionid, - 'itemnumber' => 2, 'value' => 4)); + AND qdd.name = ?", [$questiondata->id, 'b']); + $DB->set_field('question_dataset_definitions', 'itemcount', 2, ['id' => $adefinitionid]); + $DB->set_field('question_dataset_definitions', 'itemcount', 2, ['id' => $bdefinitionid]); + $DB->insert_record('question_dataset_items', ['definition' => $adefinitionid, + 'itemnumber' => 1, 'value' => 3]); + $DB->insert_record('question_dataset_items', ['definition' => $bdefinitionid, + 'itemnumber' => 1, 'value' => 7]); + $DB->insert_record('question_dataset_items', ['definition' => $adefinitionid, + 'itemnumber' => 2, 'value' => 6]); + $DB->insert_record('question_dataset_items', ['definition' => $bdefinitionid, + 'itemnumber' => 2, 'value' => 4]); $question = question_bank::load_question($questiondata->id); @@ -96,7 +96,7 @@ class least_used_variant_strategy_testcase extends advanced_testcase { $quba1->set_preferred_behaviour('deferredfeedback'); $slot1 = $quba1->add_question($question); $quba1->start_all_questions(new core_question\engine\variants\least_used_strategy( - $quba1, new qubaid_list(array()))); + $quba1, new qubaid_list([]))); question_engine::save_questions_usage_by_activity($quba1); $variant1 = $quba1->get_variant($slot1); @@ -105,7 +105,7 @@ class least_used_variant_strategy_testcase extends advanced_testcase { $quba2->set_preferred_behaviour('deferredfeedback'); $slot2 = $quba2->add_question($question); $quba2->start_all_questions(new core_question\engine\variants\least_used_strategy( - $quba1, new qubaid_list(array($quba1->get_id())))); + $quba1, new qubaid_list([$quba1->get_id()]))); question_engine::save_questions_usage_by_activity($quba2); $variant2 = $quba2->get_variant($slot2); @@ -116,7 +116,7 @@ class least_used_variant_strategy_testcase extends advanced_testcase { $quba3->set_preferred_behaviour('deferredfeedback'); $slot3 = $quba3->add_question($question); $quba3->start_all_questions(new core_question\engine\variants\least_used_strategy( - $quba1, new qubaid_list(array($quba1->get_id(), $quba2->get_id())))); + $quba1, new qubaid_list([$quba1->get_id(), $quba2->get_id()]))); $variant3 = $quba3->get_variant($slot3); $this->assertTrue($variant3 == $variant1 || $variant3 == $variant2); diff --git a/question/tests/privacy_provider_test.php b/question/tests/privacy_provider_test.php index bd0c35da943..3c657499ae1 100644 --- a/question/tests/privacy_provider_test.php +++ b/question/tests/privacy_provider_test.php @@ -305,17 +305,17 @@ class core_question_privacy_provider_testcase extends \core_privacy\tests\provid // Q4 - Created by the other user, Modified by the other user. // Q5 - Created by the UUT, Modified by the UUT, but in a different context. $this->setUser($user); - $q1 = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id)); - $q2 = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id)); + $q1 = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]); + $q2 = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]); $this->setUser($otheruser); $questiongenerator->update_question($q2); - $q3 = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id)); - $q4 = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id)); + $q3 = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]); + $q4 = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]); $this->setUser($user); $questiongenerator->update_question($q3); - $q5 = $questiongenerator->create_question('shortanswer', null, array('category' => $othercat->id)); + $q5 = $questiongenerator->create_question('shortanswer', null, ['category' => $othercat->id]); $approvedcontextlist = new \core_privacy\tests\request\approved_contextlist( $user, diff --git a/question/tests/question_bank_column_test.php b/question/tests/question_bank_column_test.php index 4d4b95837df..21648a203ac 100644 --- a/question/tests/question_bank_column_test.php +++ b/question/tests/question_bank_column_test.php @@ -44,7 +44,7 @@ class question_bank_column_testcase extends advanced_testcase { public function test_column_header_multi_sort_no_tooltips() { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); - $questionbank = new core_question\bank\view( + $questionbank = new core_question\local\bank\view( new question_edit_contexts(context_course::instance($course->id)), new moodle_url('/'), $course @@ -67,8 +67,8 @@ class question_bank_column_testcase extends advanced_testcase { $columnbase->display_header(); $output = ob_get_clean(); - $this->assertStringContainsString(' title="Sort by Apple ascending">Apple', $output); - $this->assertStringContainsString(' title="Sort by Banana ascending">Banana', $output); + $this->assertStringContainsString(' title="Sort by Apple ascending">', $output); + $this->assertStringContainsString(' title="Sort by Banana ascending">', $output); } /** @@ -78,7 +78,7 @@ class question_bank_column_testcase extends advanced_testcase { public function test_column_header_multi_sort_with_tooltips() { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(); - $questionbank = new core_question\bank\view( + $questionbank = new core_question\local\bank\view( new question_edit_contexts(context_course::instance($course->id)), new moodle_url('/'), $course @@ -103,7 +103,7 @@ class question_bank_column_testcase extends advanced_testcase { $columnbase->display_header(); $output = ob_get_clean(); - $this->assertStringContainsString(' title="Sort by Apple Tooltips ascending">Apple', $output); - $this->assertStringContainsString(' title="Sort by Banana Tooltips ascending">Banana', $output); + $this->assertStringContainsString(' title="Sort by Apple Tooltips ascending">', $output); + $this->assertStringContainsString(' title="Sort by Banana Tooltips ascending">', $output); } } diff --git a/question/tests/random_question_loader_test.php b/question/tests/random_question_loader_test.php index eec3dde5083..a2c2186770d 100644 --- a/question/tests/random_question_loader_test.php +++ b/question/tests/random_question_loader_test.php @@ -15,7 +15,7 @@ // along with Moodle. If not, see . /** - * Tests for the {@link core_question\bank\random_question_loader} class. + * Tests for the {@see core_question\local\bank\random_question_loader} class. * * @package core_question * @copyright 2015 The Open University @@ -26,7 +26,7 @@ defined('MOODLE_INTERNAL') || die(); /** - * Tests for the {@link core_question\bank\random_question_loader} class. + * Tests for the {@see core_question\local\bank\random_question_loader} class. * * @copyright 2015 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later @@ -38,7 +38,7 @@ class random_question_loader_testcase extends advanced_testcase { $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); $cat = $generator->create_question_category(); - $loader = new \core_question\bank\random_question_loader(new qubaid_list(array())); + $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([])); $this->assertNull($loader->get_next_question_id($cat->id, 0)); $this->assertNull($loader->get_next_question_id($cat->id, 1)); @@ -46,7 +46,7 @@ class random_question_loader_testcase extends advanced_testcase { public function test_unknown_category_behaves_like_empty() { // It is up the caller to make sure the category id is valid. - $loader = new \core_question\bank\random_question_loader(new qubaid_list(array())); + $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([])); $this->assertNull($loader->get_next_question_id(-1, 1)); } @@ -55,8 +55,8 @@ class random_question_loader_testcase extends advanced_testcase { $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); $cat = $generator->create_question_category(); - $info = $generator->create_question('description', null, array('category' => $cat->id)); - $loader = new \core_question\bank\random_question_loader(new qubaid_list(array())); + $info = $generator->create_question('description', null, ['category' => $cat->id]); + $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([])); $this->assertNull($loader->get_next_question_id($cat->id, 0)); } @@ -67,9 +67,9 @@ class random_question_loader_testcase extends advanced_testcase { $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); $cat = $generator->create_question_category(); - $question1 = $generator->create_question('shortanswer', null, array('category' => $cat->id)); - $DB->set_field('question', 'hidden', 1, array('id' => $question1->id)); - $loader = new \core_question\bank\random_question_loader(new qubaid_list(array())); + $question1 = $generator->create_question('shortanswer', null, ['category' => $cat->id]); + $DB->set_field('question', 'hidden', 1, ['id' => $question1->id]); + $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([])); $this->assertNull($loader->get_next_question_id($cat->id, 0)); } @@ -79,8 +79,8 @@ class random_question_loader_testcase extends advanced_testcase { $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); $cat = $generator->create_question_category(); - $question1 = $generator->create_question('multianswer', null, array('category' => $cat->id)); - $loader = new \core_question\bank\random_question_loader(new qubaid_list(array())); + $question1 = $generator->create_question('multianswer', null, ['category' => $cat->id]); + $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([])); $this->assertEquals($question1->id, $loader->get_next_question_id($cat->id, 0)); $this->assertNull($loader->get_next_question_id($cat->id, 0)); @@ -93,9 +93,9 @@ class random_question_loader_testcase extends advanced_testcase { $cat = $generator->create_question_category(); $course = $this->getDataGenerator()->create_course(); - $quiz = $this->getDataGenerator()->create_module('quiz', array('course' => $course)); + $quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course]); quiz_add_random_questions($quiz, 1, $cat->id, 1, false); - $loader = new \core_question\bank\random_question_loader(new qubaid_list(array())); + $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([])); $this->assertNull($loader->get_next_question_id($cat->id, 0)); } @@ -105,8 +105,8 @@ class random_question_loader_testcase extends advanced_testcase { $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); $cat = $generator->create_question_category(); - $question1 = $generator->create_question('shortanswer', null, array('category' => $cat->id)); - $loader = new \core_question\bank\random_question_loader(new qubaid_list(array())); + $question1 = $generator->create_question('shortanswer', null, ['category' => $cat->id]); + $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([])); $this->assertEquals($question1->id, $loader->get_next_question_id($cat->id, 1)); $this->assertNull($loader->get_next_question_id($cat->id, 0)); @@ -117,15 +117,15 @@ class random_question_loader_testcase extends advanced_testcase { $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); $cat = $generator->create_question_category(); - $question1 = $generator->create_question('shortanswer', null, array('category' => $cat->id)); - $question2 = $generator->create_question('shortanswer', null, array('category' => $cat->id)); - $loader = new \core_question\bank\random_question_loader(new qubaid_list(array())); + $question1 = $generator->create_question('shortanswer', null, ['category' => $cat->id]); + $question2 = $generator->create_question('shortanswer', null, ['category' => $cat->id]); + $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([])); - $questionids = array(); + $questionids = []; $questionids[] = $loader->get_next_question_id($cat->id, 0); $questionids[] = $loader->get_next_question_id($cat->id, 0); sort($questionids); - $this->assertEquals(array($question1->id, $question2->id), $questionids); + $this->assertEquals([$question1->id, $question2->id], $questionids); $this->assertNull($loader->get_next_question_id($cat->id, 1)); } @@ -135,10 +135,10 @@ class random_question_loader_testcase extends advanced_testcase { $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); $cat1 = $generator->create_question_category(); - $cat2 = $generator->create_question_category(array('parent' => $cat1->id)); - $question1 = $generator->create_question('shortanswer', null, array('category' => $cat1->id)); - $question2 = $generator->create_question('shortanswer', null, array('category' => $cat2->id)); - $loader = new \core_question\bank\random_question_loader(new qubaid_list(array())); + $cat2 = $generator->create_question_category(['parent' => $cat1->id]); + $question1 = $generator->create_question('shortanswer', null, ['category' => $cat1->id]); + $question2 = $generator->create_question('shortanswer', null, ['category' => $cat2->id]); + $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([])); $this->assertEquals($question2->id, $loader->get_next_question_id($cat2->id, 1)); $this->assertEquals($question1->id, $loader->get_next_question_id($cat1->id, 1)); @@ -151,9 +151,9 @@ class random_question_loader_testcase extends advanced_testcase { $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); $cat = $generator->create_question_category(); - $question1 = $generator->create_question('shortanswer', null, array('category' => $cat->id)); - $question2 = $generator->create_question('shortanswer', null, array('category' => $cat->id)); - $loader = new \core_question\bank\random_question_loader(new qubaid_list(array()), + $question1 = $generator->create_question('shortanswer', null, ['category' => $cat->id]); + $question2 = $generator->create_question('shortanswer', null, ['category' => $cat->id]); + $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([]), array($question2->id => 2)); $this->assertEquals($question1->id, $loader->get_next_question_id($cat->id, 0)); @@ -165,8 +165,8 @@ class random_question_loader_testcase extends advanced_testcase { $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); $cat = $generator->create_question_category(); - $question1 = $generator->create_question('shortanswer', null, array('category' => $cat->id)); - $question2 = $generator->create_question('shortanswer', null, array('category' => $cat->id)); + $question1 = $generator->create_question('shortanswer', null, ['category' => $cat->id]); + $question2 = $generator->create_question('shortanswer', null, ['category' => $cat->id]); $quba = question_engine::make_questions_usage_by_activity('test', context_system::instance()); $quba->set_preferred_behaviour('deferredfeedback'); $question = question_bank::load_question($question2->id); @@ -175,7 +175,7 @@ class random_question_loader_testcase extends advanced_testcase { $quba->start_all_questions(); question_engine::save_questions_usage_by_activity($quba); - $loader = new \core_question\bank\random_question_loader(new qubaid_list(array($quba->get_id()))); + $loader = new \core_question\local\bank\random_question_loader(new qubaid_list(array($quba->get_id()))); $this->assertEquals($question1->id, $loader->get_next_question_id($cat->id, 0)); $this->assertEquals($question2->id, $loader->get_next_question_id($cat->id, 0)); @@ -187,7 +187,7 @@ class random_question_loader_testcase extends advanced_testcase { $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); $cat = $generator->create_question_category(); - $loader = new \core_question\bank\random_question_loader(new qubaid_list(array())); + $loader = new \core_question\local\bank\random_question_loader(new qubaid_list(array())); $this->assertFalse($loader->is_question_available($cat->id, 0, 1)); $this->assertFalse($loader->is_question_available($cat->id, 1, 1)); @@ -199,7 +199,7 @@ class random_question_loader_testcase extends advanced_testcase { $cat = $generator->create_question_category(); $info = $generator->create_question('description', null, array('category' => $cat->id)); - $loader = new \core_question\bank\random_question_loader(new qubaid_list(array())); + $loader = new \core_question\local\bank\random_question_loader(new qubaid_list(array())); $this->assertFalse($loader->is_question_available($cat->id, 0, $info->id)); $this->assertFalse($loader->is_question_available($cat->id, 1, $info->id)); @@ -211,7 +211,7 @@ class random_question_loader_testcase extends advanced_testcase { $cat = $generator->create_question_category(); $question1 = $generator->create_question('shortanswer', null, array('category' => $cat->id)); - $loader = new \core_question\bank\random_question_loader(new qubaid_list(array())); + $loader = new \core_question\local\bank\random_question_loader(new qubaid_list(array())); $this->assertTrue($loader->is_question_available($cat->id, 0, $question1->id)); $this->assertFalse($loader->is_question_available($cat->id, 0, $question1->id)); @@ -224,54 +224,54 @@ class random_question_loader_testcase extends advanced_testcase { */ public function get_questions_test_cases() { return [ - 'empty category' => [ - 'categoryindex' => 'emptycat', - 'includesubcategories' => false, - 'usetagnames' => [], - 'expectedquestionindexes' => [] - ], - 'single category' => [ - 'categoryindex' => 'cat1', - 'includesubcategories' => false, - 'usetagnames' => [], - 'expectedquestionindexes' => ['cat1q1', 'cat1q2'] - ], - 'include sub category' => [ - 'categoryindex' => 'cat1', - 'includesubcategories' => true, - 'usetagnames' => [], - 'expectedquestionindexes' => ['cat1q1', 'cat1q2', 'subcatq1', 'subcatq2'] - ], - 'single category with tags' => [ - 'categoryindex' => 'cat1', - 'includesubcategories' => false, - 'usetagnames' => ['cat1'], - 'expectedquestionindexes' => ['cat1q1'] - ], - 'include sub category with tag on parent' => [ - 'categoryindex' => 'cat1', - 'includesubcategories' => true, - 'usetagnames' => ['cat1'], - 'expectedquestionindexes' => ['cat1q1'] - ], - 'include sub category with tag on sub' => [ - 'categoryindex' => 'cat1', - 'includesubcategories' => true, - 'usetagnames' => ['subcat'], - 'expectedquestionindexes' => ['subcatq1'] - ], - 'include sub category with same tag on parent and sub' => [ - 'categoryindex' => 'cat1', - 'includesubcategories' => true, - 'usetagnames' => ['foo'], - 'expectedquestionindexes' => ['cat1q1', 'subcatq1'] - ], - 'include sub category with tag not matching' => [ - 'categoryindex' => 'cat1', - 'includesubcategories' => true, - 'usetagnames' => ['cat1', 'cat2'], - 'expectedquestionindexes' => [] - ] + 'empty category' => [ + 'categoryindex' => 'emptycat', + 'includesubcategories' => false, + 'usetagnames' => [], + 'expectedquestionindexes' => [] + ], + 'single category' => [ + 'categoryindex' => 'cat1', + 'includesubcategories' => false, + 'usetagnames' => [], + 'expectedquestionindexes' => ['cat1q1', 'cat1q2'] + ], + 'include sub category' => [ + 'categoryindex' => 'cat1', + 'includesubcategories' => true, + 'usetagnames' => [], + 'expectedquestionindexes' => ['cat1q1', 'cat1q2', 'subcatq1', 'subcatq2'] + ], + 'single category with tags' => [ + 'categoryindex' => 'cat1', + 'includesubcategories' => false, + 'usetagnames' => ['cat1'], + 'expectedquestionindexes' => ['cat1q1'] + ], + 'include sub category with tag on parent' => [ + 'categoryindex' => 'cat1', + 'includesubcategories' => true, + 'usetagnames' => ['cat1'], + 'expectedquestionindexes' => ['cat1q1'] + ], + 'include sub category with tag on sub' => [ + 'categoryindex' => 'cat1', + 'includesubcategories' => true, + 'usetagnames' => ['subcat'], + 'expectedquestionindexes' => ['subcatq1'] + ], + 'include sub category with same tag on parent and sub' => [ + 'categoryindex' => 'cat1', + 'includesubcategories' => true, + 'usetagnames' => ['foo'], + 'expectedquestionindexes' => ['cat1q1', 'subcatq1'] + ], + 'include sub category with tag not matching' => [ + 'categoryindex' => 'cat1', + 'includesubcategories' => true, + 'usetagnames' => ['cat1', 'cat2'], + 'expectedquestionindexes' => [] + ] ]; } @@ -301,20 +301,20 @@ class random_question_loader_testcase extends advanced_testcase { * @param string[] $expectedquestionindexes The questions expected in the result */ public function test_get_questions_variations( - $categoryindex, - $includesubcategories, - $usetagnames, - $expectedquestionindexes + $categoryindex, + $includesubcategories, + $usetagnames, + $expectedquestionindexes ) { $this->resetAfterTest(); $categories = []; $questions = []; $tagnames = [ - 'cat1', - 'cat2', - 'subcat', - 'foo' + 'cat1', + 'cat2', + 'subcat', + 'foo' ]; $collid = core_tag_collection::get_default(); $tags = core_tag_tag::create_if_missing($collid, $tagnames); @@ -345,7 +345,7 @@ class random_question_loader_testcase extends advanced_testcase { return $tags[$tagname]->id; }, $usetagnames); - $loader = new \core_question\bank\random_question_loader(new qubaid_list([])); + $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([])); $result = $loader->get_questions($category->id, $includesubcategories, $tagids); // Generate the expected question set. $expectedquestions = array_map(function($index) use ($questions) { @@ -370,7 +370,7 @@ class random_question_loader_testcase extends advanced_testcase { $tagids = []; $limit = 1; $offset = 0; - $loader = new \core_question\bank\random_question_loader(new qubaid_list([])); + $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([])); list($category, $questions) = $this->create_category_and_questions($numberofquestions); // Sort the questions by id to match the ordering of the get_questions @@ -387,11 +387,11 @@ class random_question_loader_testcase extends advanced_testcase { for ($i = 0; $i < $numberofquestions; $i++) { $result = $loader->get_questions( - $category->id, - $includesubcategories, - $tagids, - $limit, - $offset + $category->id, + $includesubcategories, + $tagids, + $limit, + $offset ); $this->assertCount($limit, $result); @@ -413,16 +413,16 @@ class random_question_loader_testcase extends advanced_testcase { $limit = 10; $offset = 0; $fields = ['id', 'name']; - $loader = new \core_question\bank\random_question_loader(new qubaid_list([])); + $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([])); list($category, $questions) = $this->create_category_and_questions(1); $result = $loader->get_questions( - $category->id, - $includesubcategories, - $tagids, - $limit, - $offset, - $fields + $category->id, + $includesubcategories, + $tagids, + $limit, + $offset, + $fields ); $expectedquestion = array_shift($questions); @@ -440,54 +440,54 @@ class random_question_loader_testcase extends advanced_testcase { */ public function count_questions_test_cases() { return [ - 'empty category' => [ - 'categoryindex' => 'emptycat', - 'includesubcategories' => false, - 'usetagnames' => [], - 'expectedcount' => 0 - ], - 'single category' => [ - 'categoryindex' => 'cat1', - 'includesubcategories' => false, - 'usetagnames' => [], - 'expectedcount' => 2 - ], - 'include sub category' => [ - 'categoryindex' => 'cat1', - 'includesubcategories' => true, - 'usetagnames' => [], - 'expectedcount' => 4 - ], - 'single category with tags' => [ - 'categoryindex' => 'cat1', - 'includesubcategories' => false, - 'usetagnames' => ['cat1'], - 'expectedcount' => 1 - ], - 'include sub category with tag on parent' => [ - 'categoryindex' => 'cat1', - 'includesubcategories' => true, - 'usetagnames' => ['cat1'], - 'expectedcount' => 1 - ], - 'include sub category with tag on sub' => [ - 'categoryindex' => 'cat1', - 'includesubcategories' => true, - 'usetagnames' => ['subcat'], - 'expectedcount' => 1 - ], - 'include sub category with same tag on parent and sub' => [ - 'categoryindex' => 'cat1', - 'includesubcategories' => true, - 'usetagnames' => ['foo'], - 'expectedcount' => 2 - ], - 'include sub category with tag not matching' => [ - 'categoryindex' => 'cat1', - 'includesubcategories' => true, - 'usetagnames' => ['cat1', 'cat2'], - 'expectedcount' => 0 - ] + 'empty category' => [ + 'categoryindex' => 'emptycat', + 'includesubcategories' => false, + 'usetagnames' => [], + 'expectedcount' => 0 + ], + 'single category' => [ + 'categoryindex' => 'cat1', + 'includesubcategories' => false, + 'usetagnames' => [], + 'expectedcount' => 2 + ], + 'include sub category' => [ + 'categoryindex' => 'cat1', + 'includesubcategories' => true, + 'usetagnames' => [], + 'expectedcount' => 4 + ], + 'single category with tags' => [ + 'categoryindex' => 'cat1', + 'includesubcategories' => false, + 'usetagnames' => ['cat1'], + 'expectedcount' => 1 + ], + 'include sub category with tag on parent' => [ + 'categoryindex' => 'cat1', + 'includesubcategories' => true, + 'usetagnames' => ['cat1'], + 'expectedcount' => 1 + ], + 'include sub category with tag on sub' => [ + 'categoryindex' => 'cat1', + 'includesubcategories' => true, + 'usetagnames' => ['subcat'], + 'expectedcount' => 1 + ], + 'include sub category with same tag on parent and sub' => [ + 'categoryindex' => 'cat1', + 'includesubcategories' => true, + 'usetagnames' => ['foo'], + 'expectedcount' => 2 + ], + 'include sub category with tag not matching' => [ + 'categoryindex' => 'cat1', + 'includesubcategories' => true, + 'usetagnames' => ['cat1', 'cat2'], + 'expectedcount' => 0 + ] ]; } @@ -517,20 +517,20 @@ class random_question_loader_testcase extends advanced_testcase { * @param int $expectedcount The number of questions expected in the result */ public function test_count_questions_variations( - $categoryindex, - $includesubcategories, - $usetagnames, - $expectedcount + $categoryindex, + $includesubcategories, + $usetagnames, + $expectedcount ) { $this->resetAfterTest(); $categories = []; $questions = []; $tagnames = [ - 'cat1', - 'cat2', - 'subcat', - 'foo' + 'cat1', + 'cat2', + 'subcat', + 'foo' ]; $collid = core_tag_collection::get_default(); $tags = core_tag_tag::create_if_missing($collid, $tagnames); @@ -561,7 +561,7 @@ class random_question_loader_testcase extends advanced_testcase { return $tags[$tagname]->id; }, $usetagnames); - $loader = new \core_question\bank\random_question_loader(new qubaid_list([])); + $loader = new \core_question\local\bank\random_question_loader(new qubaid_list([])); $result = $loader->count_questions($category->id, $includesubcategories, $tagids); // Ensure the result matches what was expected. diff --git a/question/type/calculated/datasetitems_form.php b/question/type/calculated/datasetitems_form.php index a29cc7bfe37..f566ad60cfd 100644 --- a/question/type/calculated/datasetitems_form.php +++ b/question/type/calculated/datasetitems_form.php @@ -334,12 +334,20 @@ class question_dataset_dependent_items_form extends question_wizard_form { // Submit buttons. if ($this->noofitems > 0) { - $buttonarray = array(); + $buttonarray = []; $buttonarray[] = $mform->createElement( 'submit', 'savechanges', get_string('savechanges')); - $previewlink = $PAGE->get_renderer('core_question')->question_preview_link( + // Todo MDL-72004 changes for class renaming and default sort. + if (class_exists('qbank_previewquestion\\preview_action_column')) { + if (\core\plugininfo\qbank::is_plugin_enabled('qbank_previewquestion')) { + $previewlink = $PAGE->get_renderer('qbank_previewquestion')->question_preview_link( + $this->question->id, $this->categorycontext, true); + } + } else { + $previewlink = $PAGE->get_renderer('core_question')->question_preview_link( $this->question->id, $this->categorycontext, true); + } $buttonarray[] = $mform->createElement('static', 'previewlink', '', $previewlink); $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false); diff --git a/question/type/edit_question_form.php b/question/type/edit_question_form.php index c152fb2b13d..384e689128b 100644 --- a/question/type/edit_question_form.php +++ b/question/type/edit_question_form.php @@ -233,7 +233,7 @@ abstract class question_edit_form extends question_wizard_form { $a->user = get_string('unknown', 'question'); } $mform->addElement('static', 'created', get_string('created', 'question'), - get_string('byandon', 'question', $a)); + get_string('byandon', 'question', $a)); if (!empty($this->question->modifiedby)) { $a = new stdClass(); $a->time = userdate($this->question->timemodified); @@ -254,10 +254,18 @@ abstract class question_edit_form extends question_wizard_form { $buttonarray = array(); $buttonarray[] = $mform->createElement('submit', 'updatebutton', - get_string('savechangesandcontinueediting', 'question')); + get_string('savechangesandcontinueediting', 'question')); if ($this->can_preview()) { - $previewlink = $PAGE->get_renderer('core_question')->question_preview_link( - $this->question->id, $this->context, true); + // Todo MDL-72004 changes for class renaming and default sort. + if (class_exists('qbank_previewquestion\\preview_action_column')) { + if (\core\plugininfo\qbank::is_plugin_enabled('qbank_previewquestion')) { + $previewlink = $PAGE->get_renderer('qbank_previewquestion')->question_preview_link( + $this->question->id, $this->context, true); + } + } else { + $previewlink = $PAGE->get_renderer('core_question')->question_preview_link( + $this->question->id, $this->context, true); + } $buttonarray[] = $mform->createElement('static', 'previewlink', '', $previewlink); } @@ -267,7 +275,7 @@ abstract class question_edit_form extends question_wizard_form { $this->add_action_buttons(true, get_string('savechanges')); if ((!empty($this->question->id)) && (!($this->question->formoptions->canedit || - $this->question->formoptions->cansaveasnew))) { + $this->question->formoptions->cansaveasnew))) { $mform->hardFreezeAllVisibleExcept(array('categorymoveto', 'buttonar', 'currentgrp')); } } @@ -309,7 +317,7 @@ abstract class question_edit_form extends question_wizard_form { $answeroptions[] = $mform->createElement('select', 'fraction', get_string('gradenoun'), $gradeoptions); $repeated[] = $mform->createElement('group', 'answeroptions', - $label, $answeroptions, null, false); + $label, $answeroptions, null, false); $repeated[] = $mform->createElement('editor', 'feedback', get_string('feedback', 'question'), array('rows' => 5), $this->editoroptions); $repeatedoptions['answer']['type'] = PARAM_RAW; @@ -377,7 +385,7 @@ abstract class question_edit_form extends question_wizard_form { // and the question isn't a course or activity level question then // allow course tags to be added to the course. $coursetagheader = get_string('questionformtagheader', 'core_question', - $editingcoursecontext->get_context_name(true)); + $editingcoursecontext->get_context_name(true)); $mform->addElement('header', 'coursetagsheader', $coursetagheader); $mform->addElement('autocomplete', 'coursetags', get_string('tags'), $tagstrings, $options); @@ -400,7 +408,7 @@ abstract class question_edit_form extends question_wizard_form { protected function add_per_answer_fields(&$mform, $label, $gradeoptions, $minoptions = QUESTION_NUMANS_START, $addoptions = QUESTION_NUMANS_ADD) { $mform->addElement('header', 'answerhdr', - get_string('answers', 'question'), ''); + get_string('answers', 'question'), ''); $mform->setExpanded('answerhdr', 1); $answersoption = ''; $repeatedoptions = array(); @@ -434,8 +442,8 @@ abstract class question_edit_form extends question_wizard_form { $fields = array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback'); foreach ($fields as $feedbackname) { $element = $mform->addElement('editor', $feedbackname, - get_string($feedbackname, 'question'), - array('rows' => 5), $this->editoroptions); + get_string($feedbackname, 'question'), + array('rows' => 5), $this->editoroptions); $mform->setType($feedbackname, PARAM_RAW); // Using setValue() as setDefault() does not work for the editor class. $element->setValue(array('text' => get_string($feedbackname.'default', 'question'))); @@ -476,7 +484,7 @@ abstract class question_edit_form extends question_wizard_form { if (count($optionelements)) { $repeated[] = $mform->createElement('group', 'hintoptions', - get_string('hintnoptions', 'question'), $optionelements, null, false); + get_string('hintnoptions', 'question'), $optionelements, null, false); } return array($repeated, $repeatedoptions); @@ -620,13 +628,13 @@ abstract class question_edit_form extends question_wizard_form { // Prepare the feedback editor to display files in draft area. $draftitemid = file_get_submitted_draft_itemid('answer['.$key.']'); $question->answer[$key]['text'] = file_prepare_draft_area( - $draftitemid, // Draftid - $this->context->id, // context - 'question', // component - 'answer', // filarea - !empty($answer->id) ? (int) $answer->id : null, // itemid - $this->fileoptions, // options - $answer->answer // text. + $draftitemid, // Draftid. + $this->context->id, // Context. + 'question', // Component. + 'answer', // Filarea. + !empty($answer->id) ? (int) $answer->id : null, // Itemid. + $this->fileoptions, // Options. + $answer->answer // Text. ); $question->answer[$key]['itemid'] = $draftitemid; $question->answer[$key]['format'] = $answer->answerformat; @@ -651,13 +659,13 @@ abstract class question_edit_form extends question_wizard_form { // Prepare the feedback editor to display files in draft area. $draftitemid = file_get_submitted_draft_itemid('feedback['.$key.']'); $question->feedback[$key]['text'] = file_prepare_draft_area( - $draftitemid, // Draftid - $this->context->id, // context - 'question', // component - 'answerfeedback', // filarea - !empty($answer->id) ? (int) $answer->id : null, // itemid - $this->fileoptions, // options - $answer->feedback // text. + $draftitemid, // Draftid. + $this->context->id, // Context. + 'question', // Component. + 'answerfeedback', // Filarea. + !empty($answer->id) ? (int) $answer->id : null, // Itemid. + $this->fileoptions, // Options. + $answer->feedback // Text. ); $question->feedback[$key]['itemid'] = $draftitemid; $question->feedback[$key]['format'] = $answer->feedbackformat; @@ -742,13 +750,13 @@ abstract class question_edit_form extends question_wizard_form { $draftid = file_get_submitted_draft_itemid($feedbackname); $feedback = array(); $feedback['text'] = file_prepare_draft_area( - $draftid, // Draftid - $this->context->id, // context - 'question', // component - $feedbackname, // filarea - !empty($question->id) ? (int) $question->id : null, // itemid - $this->fileoptions, // options - $question->options->$feedbackname // text. + $draftid, // Draftid. + $this->context->id, // Context. + 'question', // Component. + $feedbackname, // Filarea. + !empty($question->id) ? (int) $question->id : null, // Itemid. + $this->fileoptions, // Options. + $question->options->$feedbackname // Text. ); $feedbackformat = $feedbackname . 'format'; $feedback['format'] = $question->options->$feedbackformat; @@ -782,13 +790,13 @@ abstract class question_edit_form extends question_wizard_form { // Prepare feedback editor to display files in draft area. $draftitemid = file_get_submitted_draft_itemid('hint['.$key.']'); $question->hint[$key]['text'] = file_prepare_draft_area( - $draftitemid, // Draftid - $this->context->id, // context - 'question', // component - 'hint', // filarea - !empty($hint->id) ? (int) $hint->id : null, // itemid - $this->fileoptions, // options - $hint->hint // text. + $draftitemid, // Draftid. + $this->context->id, // Context. + 'question', // Component. + 'hint', // Filarea. + !empty($hint->id) ? (int) $hint->id : null, // Itemid. + $this->fileoptions, // Options. + $hint->hint // Text. ); $question->hint[$key]['itemid'] = $draftitemid; $question->hint[$key]['format'] = $hint->hintformat; diff --git a/question/upgrade.txt b/question/upgrade.txt index 6354f1ff602..f2b7fedbf61 100644 --- a/question/upgrade.txt +++ b/question/upgrade.txt @@ -1,5 +1,12 @@ This files describes API changes for code that uses the question API. +=== 4.0 == + +1) Previously, the questionbank api classes were coupled in one place. Now the classes + are divided in two different parts, base classes and feature classes. All the base + classes are moved classes/local/bank and all the feature classes will be moved to + the plugin for that feature. + === 3.9 == 1) For years, the ..._questions_in_use callback has been the right way for plugins to