MDL-74054 core_question: Add additional plugin points

This refactors the question bank view class to support new plugin points
required for the question bank UI customisation features.
This commit is contained in:
Nathan Nguyen
2023-09-22 10:53:51 +08:00
committed by Andrew Nicols
parent feef716c91
commit 2be0e10a80
31 changed files with 921 additions and 434 deletions
@@ -1,10 +0,0 @@
define("qbank_columnsortorder/sort_columns",["exports","core/ajax","core/notification","core/sortable_list","jquery"],(function(_exports,_ajax,_notification,_sortable_list,_jquery){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
/**
* Javascript for sorting columns in question bank view.
*
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Ghaly Marc-Alexandre <marc-alexandreghaly@catalyst-ca.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_sortable_list=_interopRequireDefault(_sortable_list),_jquery=_interopRequireDefault(_jquery);const setOrder=columns=>(0,_ajax.call)([{methodname:"qbank_columnsortorder_set_columnbank_order",args:{columns:columns}}])[0],getColumnOrder=listRoot=>{const columns=Array.from(listRoot.querySelectorAll("[data-pluginname]")).map((column=>column.dataset.pluginname));return columns.filter(((value,index)=>columns.indexOf(value)===index))};_exports.init=id=>{(listRoot=>{new _sortable_list.default(".list",{moveHandlerSelector:".item"}),(0,_jquery.default)(".item").on(_sortable_list.default.EVENTS.DROP,(()=>{const columns=getColumnOrder(listRoot);setOrder(columns).catch(_notification.exception),listRoot.querySelectorAll(".item").forEach((item=>item.classList.remove("active")))})),(0,_jquery.default)(".item").on(_sortable_list.default.EVENTS.DRAGSTART,(event=>{event.currentTarget.classList.add("active")}))})(document.querySelector("#".concat(id)))}}));
//# sourceMappingURL=sort_columns.min.js.map
@@ -1 +0,0 @@
{"version":3,"file":"sort_columns.min.js","sources":["../src/sort_columns.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Javascript for sorting columns in question bank view.\n *\n * @copyright 2021 Catalyst IT Australia Pty Ltd\n * @author Ghaly Marc-Alexandre <[email protected]>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {call as fetchMany} from 'core/ajax';\nimport {exception as displayException} from 'core/notification';\nimport SortableList from 'core/sortable_list';\nimport jQuery from 'jquery';\n\n/**\n * Sets up sortable list in the column sort order page.\n * @param {Element} listRoot\n */\nconst setupSortableLists = (listRoot) => {\n new SortableList('.list', {\n moveHandlerSelector: '.item',\n });\n\n jQuery('.item').on(SortableList.EVENTS.DROP, () => {\n const columns = getColumnOrder(listRoot);\n setOrder(columns).catch(displayException);\n listRoot.querySelectorAll('.item').forEach(item => item.classList.remove('active'));\n });\n\n jQuery('.item').on(SortableList.EVENTS.DRAGSTART, (event) => {\n event.currentTarget.classList.add('active');\n });\n};\n\n/**\n * Call external function set_order - inserts the updated column in the config_plugins table.\n *\n * @param {String} columns String that contains column order.\n * @returns {Promise}\n */\nconst setOrder = columns => fetchMany([{\n methodname: 'qbank_columnsortorder_set_columnbank_order',\n args: {columns},\n}])[0];\n\n/**\n * Gets the newly reordered columns to display in the question bank view.\n * @param {Element} listRoot\n * @returns {Array}\n */\nconst getColumnOrder = listRoot => {\n const columns = Array.from(listRoot.querySelectorAll('[data-pluginname]'))\n .map(column => column.dataset.pluginname);\n\n return columns.filter((value, index) => columns.indexOf(value) === index);\n};\n\n/**\n * Initialize module\n * @param {String} id unique id for columns.\n */\nexport const init = id => {\n const listRoot = document.querySelector(`#${id}`);\n setupSortableLists(listRoot);\n};\n"],"names":["setOrder","columns","methodname","args","getColumnOrder","listRoot","Array","from","querySelectorAll","map","column","dataset","pluginname","filter","value","index","indexOf","id","SortableList","moveHandlerSelector","on","EVENTS","DROP","catch","displayException","forEach","item","classList","remove","DRAGSTART","event","currentTarget","add","setupSortableLists","document","querySelector"],"mappings":";;;;;;;sLAsDMA,SAAWC,UAAW,cAAU,CAAC,CACnCC,WAAY,6CACZC,KAAM,CAACF,QAAAA,YACP,GAOEG,eAAiBC,iBACbJ,QAAUK,MAAMC,KAAKF,SAASG,iBAAiB,sBAChDC,KAAIC,QAAUA,OAAOC,QAAQC,oBAE3BX,QAAQY,QAAO,CAACC,MAAOC,QAAUd,QAAQe,QAAQF,SAAWC,uBAOnDE,KA3CQZ,CAAAA,eACpBa,uBAAa,QAAS,CACtBC,oBAAqB,8BAGlB,SAASC,GAAGF,uBAAaG,OAAOC,MAAM,WACnCrB,QAAUG,eAAeC,UAC/BL,SAASC,SAASsB,MAAMC,yBACxBnB,SAASG,iBAAiB,SAASiB,SAAQC,MAAQA,KAAKC,UAAUC,OAAO,mCAGtE,SAASR,GAAGF,uBAAaG,OAAOQ,WAAYC,QAC/CA,MAAMC,cAAcJ,UAAUK,IAAI,cAiCtCC,CADiBC,SAASC,yBAAkBlB"}
@@ -1,79 +0,0 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Javascript for sorting columns in question bank view.
*
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Ghaly Marc-Alexandre <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {call as fetchMany} from 'core/ajax';
import {exception as displayException} from 'core/notification';
import SortableList from 'core/sortable_list';
import jQuery from 'jquery';
/**
* Sets up sortable list in the column sort order page.
* @param {Element} listRoot
*/
const setupSortableLists = (listRoot) => {
new SortableList('.list', {
moveHandlerSelector: '.item',
});
jQuery('.item').on(SortableList.EVENTS.DROP, () => {
const columns = getColumnOrder(listRoot);
setOrder(columns).catch(displayException);
listRoot.querySelectorAll('.item').forEach(item => item.classList.remove('active'));
});
jQuery('.item').on(SortableList.EVENTS.DRAGSTART, (event) => {
event.currentTarget.classList.add('active');
});
};
/**
* Call external function set_order - inserts the updated column in the config_plugins table.
*
* @param {String} columns String that contains column order.
* @returns {Promise}
*/
const setOrder = columns => fetchMany([{
methodname: 'qbank_columnsortorder_set_columnbank_order',
args: {columns},
}])[0];
/**
* Gets the newly reordered columns to display in the question bank view.
* @param {Element} listRoot
* @returns {Array}
*/
const getColumnOrder = listRoot => {
const columns = Array.from(listRoot.querySelectorAll('[data-pluginname]'))
.map(column => column.dataset.pluginname);
return columns.filter((value, index) => columns.indexOf(value) === index);
};
/**
* Initialize module
* @param {String} id unique id for columns.
*/
export const init = id => {
const listRoot = document.querySelector(`#${id}`);
setupSortableLists(listRoot);
};
@@ -21,8 +21,14 @@ defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/questionlib.php');
use context_system;
use core_question\local\bank\column_action_base;
use core_question\local\bank\column_base;
use core_question\local\bank\column_manager_base;
use core_question\local\bank\question_edit_contexts;
use core_question\local\bank\view;
use qbank_columnsortorder\local\bank\column_action_move;
use qbank_columnsortorder\local\bank\column_action_remove;
use qbank_columnsortorder\local\bank\column_action_resize;
use moodle_url;
/**
@@ -33,40 +39,113 @@ use moodle_url;
* @author Ghaly Marc-Alexandre <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class column_manager {
class column_manager extends column_manager_base {
/**
* @var array|bool Column order as set in config_plugins 'class' => 'position', ie: question_type_column => 3.
* @var array Column order as set in config_plugins 'class' => 'position', ie: question_type_column => 3.
*/
public $columnorder;
/**
* @var array|bool Disabled columns in config_plugins table.
* @var array hidden columns.
*/
public $hiddencolumns;
/**
* @var array columns with size.
*/
public $colsize;
/**
* @var array Disabled columns in config_plugins table.
*/
public $disabledcolumns;
/**
* Constructor for column_manager class.
*
* @param bool $globalsettings Only use the global default settings, ignoring user preferences?
*/
public function __construct() {
$this->columnorder = get_config('qbank_columnsortorder', 'enabledcol');
$this->disabledcolumns = get_config('qbank_columnsortorder', 'disabledcol');
public function __construct(bool $globalsettings = false) {
$this->columnorder = $this->setup_property('enabledcol', $globalsettings);
$this->hiddencolumns = $this->setup_property('hiddencols', $globalsettings);
$this->colsize = $this->setup_property('colsize', $globalsettings, 'json');
$this->disabledcolumns = $this->setup_property('disabledcol', $globalsettings);
if ($this->columnorder) {
$this->columnorder = array_flip(explode(',', $this->columnorder));
$this->columnorder = array_flip($this->columnorder);
}
if ($this->disabledcolumns) {
$this->disabledcolumns = array_flip(explode(',', $this->disabledcolumns));
$this->disabledcolumns = array_flip($this->disabledcolumns);
}
}
/**
* Return the value for the given property, based the saved user preference or config setting.
*
* If no value is currently stored, returns an empty array.
*
* @param string $setting The identifier used for the saved config and user preference settings.
* @param bool $global Only get the global default, ignoring the user preference?
* @param string $encoding The encoding used to store the property - csv or json
* @return array
*/
private function setup_property(string $setting, bool $global = false, $encoding = 'csv'): array {
$value = get_config('qbank_columnsortorder', $setting);
if (!$global) {
$value = get_user_preferences("qbank_columnsortorder_{$setting}", $value);
}
if (empty($value)) {
return [];
}
return $encoding == 'csv' ? explode(',', $value) : json_decode($value);
}
/**
* Sets column order in the qbank_columnsortorder plugin config.
*
* @param array $columns Column order to set.
* @param bool $global save this as a global default, rather than a user preference?
*/
public static function set_column_order(array $columns) : void {
public static function set_column_order(array $columns, bool $global = false) : void {
$columns = implode(',', $columns);
set_config('enabledcol', $columns, 'qbank_columnsortorder');
self::save_preference('enabledcol', $columns, $global);
}
/**
* Hidden Columns.
*
* @param array $columns hidden columns
* @param bool $global save this as a global default, rather than a user preference?
*/
public static function set_hidden_columns(array $columns, bool $global = false) : void {
$columns = implode(',', $columns);
self::save_preference('hiddencols', $columns, $global);
}
/**
* Column size.
*
* @param string $sizes columns with width
* @param bool $global save this as a global default, rather than a user preference?
*/
public static function set_column_size(string $sizes, bool $global = false) : void {
self::save_preference('colsize', $sizes, $global);
}
/**
* Save Preferences.
*
* @param string $name name of a configuration
* @param string $value value of a configuration
* @param bool $global save this as a global default, rather than a user preference?
*/
private static function save_preference(string $name, string $value, bool $global = false): void {
if ($global) {
require_capability('moodle/site:config', context_system::instance());
set_config($name, $value, 'qbank_columnsortorder');
} else {
set_user_preference("qbank_columnsortorder_{$name}", $value);
}
}
/**
@@ -74,12 +153,12 @@ class column_manager {
*
* @return view
*/
protected function get_questionbank(): view {
public function get_questionbank(): view {
$course = (object) ['id' => 0];
$context = context_system::instance();
$contexts = new question_edit_contexts($context);
// Dummy call to get the objects without error.
$questionbank = new view($contexts, new moodle_url('/question/dummyurl.php'), $course, null);
$questionbank = new view($contexts, new moodle_url('/question/bank/columnsortorder/sortcolumns.php'), $course, null);
return $questionbank;
}
@@ -250,4 +329,62 @@ class column_manager {
}
return $ordertosort;
}
/**
* Given an array of columns, set the isvisible attribute according to $this->hiddencolumns.
*
* @param column_base[] $columns
* @return array
*/
public function set_columns_visibility(array $columns): array {
foreach ($columns as $column) {
if (!is_object($column)) {
continue;
}
$column->isvisible = !in_array(get_class($column), $this->hiddencolumns);
}
return $columns;
}
/**
* Return $this->colsize mapped as an array of column name => width, excluding empty sizes.
*
* @return array
*/
public function get_colsize_map(): array {
$sizes = array_reduce($this->colsize, function($result, $colsize) {
$result[$colsize->column] = $colsize->width;
return $result;
}, []);
return array_filter($sizes);
}
/**
* Return an array of hidden columns as an array of class => column name
*
* @return array
*/
public function get_hidden_columns(): array {
return array_reduce($this->hiddencolumns, function($result, $hiddencolumn) {
$result[$hiddencolumn] = (new $hiddencolumn($this->get_questionbank()))->get_title();
return $result;
}, []);
}
public function get_column_width(column_base $column): string {
$colsizemap = $this->get_colsize_map();
$columnclass = get_class($column);
if (array_key_exists($columnclass, $colsizemap)) {
return $colsizemap[$columnclass];
}
return parent::get_column_width($column);
}
public function get_column_actions(view $qbank): array {
return [
new column_action_move($qbank),
new column_action_remove($qbank),
new column_action_resize($qbank),
];
}
}
@@ -42,7 +42,9 @@ class set_columnbank_order extends external_api {
return new external_function_parameters([
'columns' => new external_multiple_structure(
new external_value(PARAM_TEXT, 'Plugin name for the column', VALUE_REQUIRED)
)
),
'global' => new external_value(PARAM_BOOL, 'Set global config setting, rather than user preference',
VALUE_DEFAULT, false),
]);
}
@@ -54,16 +56,28 @@ class set_columnbank_order extends external_api {
}
/**
* Returns the columns plugin order.
* Set columns order.
*
* @param array $columns json string representing new column order.
* @param array $columns List of column names in the desired order.
* @param bool $global Set global config setting, rather than user preference
*/
public static function execute(array $columns): void {
['columns' => $columns] = self::validate_parameters(self::execute_parameters(), ['columns' => $columns]);
public static function execute(array $columns, bool $global = false): void {
[
'columns' => $columns,
'global' => $global,
]
= self::validate_parameters(self::execute_parameters(),
[
'columns' => $columns,
'global' => $global,
]);
$context = context_system::instance();
self::validate_context($context);
require_capability('moodle/category:manage', $context);
if ($global) {
require_capability('moodle/site:config', $context);
}
column_manager::set_column_order($columns);
column_manager::set_column_order($columns, $global);
}
}
@@ -1,57 +0,0 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace qbank_columnsortorder\output;
use moodle_url;
use plugin_renderer_base;
use qbank_columnsortorder\column_manager;
/**
* Class renderer.
* @package qbank_columnsortorder
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Ghaly Marc-Alexandre <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class renderer extends plugin_renderer_base {
/**
* Render list of question bank plugin columns.
*
* @return string The rendered HTML.
*/
public function render_column_sort_ui() {
$columnsortorder = new column_manager();
$enabledcolumns = $columnsortorder->get_columns();
$disabledcolumns = $columnsortorder->get_disabled_columns();
$params = [];
foreach ($enabledcolumns as $columnname) {
$name = $columnname->name;
$colname = get_string('qbankcolumnname', 'qbank_columnsortorder', $columnname->colname);
if ($columnname->class === 'qbank_customfields\custom_field_column') {
$columnname->class .= "\\$columnname->colname";
}
$params['names'][] = ['name' => $name, 'colname' => $colname, 'class' => $columnname->class];
}
$params['disabled'] = $disabledcolumns;
$params['columnsdisabled'] = (!empty($params['disabled'])) ? true : false;
$urltoredirect = new moodle_url('/admin/settings.php', ['section' => 'manageqbanks']);
$params['urltomanageqbanks'] = get_string('qbankgotomanageqbanks', 'qbank_columnsortorder', $urltoredirect->out());
return $this->render_from_template('qbank_columnsortorder/columnsortorder', $params);
}
}
@@ -16,7 +16,8 @@
namespace qbank_columnsortorder\privacy;
use \core_privacy\local\metadata\null_provider;
use core_privacy\local\metadata\collection;
use core_privacy\local\request\writer;
/**
* Privacy provider for columnsortorder.
@@ -26,14 +27,32 @@ use \core_privacy\local\metadata\null_provider;
* @author Ghaly Marc-Alexandre <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements null_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason() : string {
return 'privacy:metadata';
class provider implements
// The forms subsystem does not store any data itself, it has no database tables.
\core_privacy\local\metadata\provider,
// The forms subsystem has user preferences.
\core_privacy\local\request\user_preference_provider {
public static function get_metadata(collection $collection): collection {
$collection->add_user_preference('enabledcol', 'privacy:metadata:preference:enabledcol');
$collection->add_user_preference('hiddencols', 'privacy:metadata:preference:hiddencols');
$collection->add_user_preference('colsize', 'privacy:metadata:preference:colsize');
return $collection;
}
public static function export_user_preferences(int $userid) {
$components = ['core_question', "qbank_history"];
foreach ($components as $component) {
$prefnames = ['enabledcol', 'hiddencols', 'colsize'];
foreach ($prefnames as $name) {
$preference = get_user_preferences("{$component}_{$name}", null, $userid);
if ($preference !== null) {
$desc = get_string('enabledcol', 'privacy:metadata:preference:enabledcol');
writer::export_user_preference('qbank_columnsortorder', "{$component}_{$name}", $preference, $desc);
}
}
}
}
}
+16 -4
View File
@@ -14,9 +14,9 @@
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* qbank_columnsortorder external functions and service definitions.
*
* @package qbank_columnsortorder
* @category webservice
* @copyright 2021 Catalyst IT Australia Pty Ltd
@@ -28,9 +28,21 @@ defined('MOODLE_INTERNAL') || die();
$functions = [
'qbank_columnsortorder_set_columnbank_order' => [
'classname' => 'qbank_columnsortorder\external\set_columnbank_order',
'classname' => 'qbank_columnsortorder\external\set_columnbank_order',
'description' => 'Sets question columns order in database',
'type' => 'write',
'ajax' => true,
'type' => 'write',
'ajax' => true,
],
'qbank_columnsortorder_set_hidden_columns' => [
'classname' => 'qbank_columnsortorder\external\set_hidden_columns',
'description' => 'Hidden Columns',
'type' => 'write',
'ajax' => true,
],
'qbank_columnsortorder_set_column_size' => [
'classname' => 'qbank_columnsortorder\external\set_column_size',
'description' => 'Column size',
'type' => 'write',
'ajax' => true,
],
];
@@ -23,11 +23,25 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['addcolumn'] = 'Add column \'{$a}\'';
$string['addcolumns'] = 'Add columns';
$string['auto'] = 'Auto';
$string['columnwidth'] = 'Column width (pixels)';
$string['invalidwidth'] = 'Width must be at least 10.';
$string['movecolumn'] = 'Move column \'{$a}\'';
$string['pluginname'] = 'Column sort order';
$string['privacy:metadata'] = 'The Column sort order question bank plugin does not store any personal data.';
$string['privacy:metadata:preference:enabledcol'] = 'The plugin saves user preference of column orders.';
$string['privacy:metadata:preference:hiddencols'] = 'The plugin saves user preference of hidden columns.';
$string['privacy:metadata:preference:colsize'] = 'The plugin saves user preference of column sizes.';
$string['qbankcolumnsortorder'] = 'Column sort order';
$string['qbankgotocolumnsort'] = 'You can change the order of the columns in the question bank on the page {$a}.';
$string['qbankcolumnsdisabled'] = 'Currently disabled question bank plugins:';
$string['qbankgotomanageqbanks'] = 'You can remove a column by disabling the plugin in <a href=\'{$a}\'>Manage question bank plugins</a>.';
$string['qbankcolumnname'] = '({$a})';
$string['qbankcolumnname'] = '{$a}';
$string['qbanksortdescription'] = 'The order in which plugins are listed below determines the order of the columns in the question bank.';
$string['resetcolumns'] = 'Reset columns';
$string['resize'] = 'Resize';
$string['resizecolumn'] = 'Resize column {$a}';
$string['removecolumn'] = 'Remove column \'{$a}\'';
$string['showhidecolumn'] = 'Show/Hide column';
$string['width'] = 'Width of \'{$a}\' in pixels';
@@ -29,9 +29,16 @@ require_once($CFG->libdir.'/adminlib.php');
admin_externalpage_setup('qbank_columnsortorder', '', ['section' => 'columnsortorder'],
'/question/bank/columnsortorder/sortcolumns.php');
$renderer = $PAGE->get_renderer('qbank_columnsortorder');
$preview = optional_param('preview', false, PARAM_BOOL);
echo $OUTPUT->header();
echo $OUTPUT->heading(new lang_string('qbankcolumnsortorder', 'qbank_columnsortorder'));
echo $renderer->render_column_sort_ui();
if ($preview) {
$columnmanager = new \qbank_columnsortorder\column_manager(true);
$preview = $columnmanager->get_questionbank()->get_preview();
echo $OUTPUT->render(new \qbank_columnsortorder\output\column_sort_preview($preview));
} else {
echo $OUTPUT->render(new \qbank_columnsortorder\output\column_sort_ui());
}
echo $OUTPUT->footer();
@@ -1,65 +0,0 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template qbank_columnsortorder/columnsortorder.mustache
Admin question bank setting template.
Context variables required for this template:
* name - plugin name
* hiddenname - class/columname
Example context (json):
{
"name": "Creatorname (creator_name_column)",
"hiddenname": "creator_name_column"
}
}}
<div>
{{#str}}qbanksortdescription, qbank_columnsortorder{{/str}}
</div>
<br>
<div class="list list-group" id="qbank_columnsortorder-{{uniqid}}">
{{#names}}
<div class="column item list-group-item list-group-item-action" data-pluginname={{class}}>
{{>core/drag_handle}}
{{name}} {{colname}}
</div>
{{/names}}
</div>
<br>
<div>
{{#columnsdisabled}}
{{#str}}qbankcolumnsdisabled, qbank_columnsortorder{{/str}}
<br>
{{/columnsdisabled}}
{{#disabled}}
<div class="list-group-item disabled">
{{disabledname}}
</div>
{{/disabled}}
</div>
{{#js}}
require(['qbank_columnsortorder/sort_columns'], function(SortColumns) {
SortColumns.init("qbank_columnsortorder-{{uniqid}}");
});
{{/js}}
<br>
<div>
{{{urltomanageqbanks}}}
</div>
@@ -23,7 +23,6 @@ use context_course;
use core_question\local\bank\question_edit_contexts;
use core_question\local\bank\view;
use moodle_url;
use qbank_columnsortorder\external\set_columnbank_order;
global $CFG;
require_once($CFG->dirroot . '/question/tests/fixtures/testable_core_question_column.php');
@@ -36,11 +35,10 @@ require_once($CFG->dirroot . '/question/classes/external.php');
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Ghaly Marc-Alexandre <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \qbank_columnsortorder\column_manager
* @covers \qbank_columnsortorder\column_manager
*/
class column_manager_test extends advanced_testcase {
/** @var \stdClass course record. */
protected $course;
@@ -53,6 +51,9 @@ class column_manager_test extends advanced_testcase {
/** @var \qbank_columnsortorder\column_manager */
protected $columnmanager;
/** @var string */
protected $randomstring;
/**
* Setup testcase.
*/
@@ -69,10 +70,74 @@ class column_manager_test extends advanced_testcase {
// Get current view columns.
$this->columns = [];
foreach ($this->questionbank->get_visiblecolumns() as $columnn) {
$this->columns[] = get_class($columnn);
foreach ($this->questionbank->get_visiblecolumns() as $column) {
$this->columns[] = get_class($column);
}
$this->columnmanager = new column_manager();
$this->randomstring = random_string();
}
public function test_settings_provider(): array {
return [
'Test set_column_order' => [
'setting' => 'enabledcol',
'function' => 'set_column_order',
'dataproperty' => 'columns',
'csv' => true
],
'Test set_hidden_columns' => [
'setting' => 'hiddencols',
'function' => 'set_hidden_columns',
'dataproperty' => 'columns',
'csv' => true
],
'Test set_column_size' => [
'setting' => 'colsize',
'function' => 'set_column_size',
'dataproperty' => 'randomstring',
'csv' => false
]
];
}
/**
* Test setting config settings
*
* @dataProvider test_settings_provider
* @param string $setting The name of the setting being saved
* @param string $function The name of the function being called
* @param string $dataproperty The property of the test class to pass to the function.
* @param bool $csv True of the data is stored as a comma-separated list.
* @return void
*/
public function test_settings(string $setting, string $function, string $dataproperty, bool $csv): void {
$data = $this->{$dataproperty};
$this->assertFalse(get_config('qbank_columnsortorder', $setting));
$this->assertEmpty(get_user_preferences('qbank_columnsortorder_' . $setting));
column_manager::{$function}($data);
$expected = $csv ? implode(',', $data) : $data;
$this->assertEquals($expected, get_config('qbank_columnsortorder', $setting));
$this->assertEmpty(get_user_preferences('qbank_columnsortorder_' . $setting));
}
/**
* Test setting user preferences
*
* @dataProvider test_settings_provider
* @param string $setting The name of the setting being saved
* @param string $function The name of the function being called
* @param string $dataproperty The property of the test class to pass to the function.
* @param bool $csv True of the data is stored as a comma-separated list.
* @return void
*/
public function test_settings_user(string $setting, string $function, string $dataproperty, bool $csv): void {
$data = $this->{$dataproperty};
$this->assertFalse(get_config('qbank_columnsortorder', $setting));
$this->assertEmpty(get_user_preferences('qbank_columnsortorder_' . $setting));
column_manager::{$function}($data, 'qbank_columnsortorder');
$expected = $csv ? implode(',', $data) : $data;
$this->assertFalse(get_config('qbank_columnsortorder', $setting));
$this->assertEquals($expected, get_user_preferences('qbank_columnsortorder_' . $setting));
}
/**
@@ -91,45 +156,59 @@ class column_manager_test extends advanced_testcase {
}
/**
* Test function sort columns method.
* The get_sorted_columns method should return the provided columns sorted according to enabledcol setting.
*
* @covers ::get_sorted_columns
* @return void
*/
public function test_get_sorted_columns(): void {
$neworder = $this->columnmanager->get_sorted_columns($this->columns);
$neworder = $this->columns;
shuffle($neworder);
set_columnbank_order::execute($neworder);
$currentconfig = get_config('qbank_columnsortorder', 'enabledcol');
$currentconfig = explode(',', $currentconfig);
ksort($currentconfig);
$this->assertSame($neworder, $currentconfig);
set_config('enabledcol', implode(',', $neworder), 'qbank_columnsortorder');
$this->columnmanager = new column_manager();
$columnstosort = [];
foreach ($this->columns as $key => $column) {
$colname = explode('\\', $column);
$columnstosort[end($colname)] = $column;
}
$sortedcolumns = $this->columnmanager->get_sorted_columns($columnstosort);
$expectedorder = ['checkbox_column' => 0];
foreach ($neworder as $key => $column) {
$colname = explode('\\', $column);
$expectedorder[end($colname)] = $column;
}
$this->assertSame($expectedorder, $sortedcolumns);
}
/**
* Test function enabing and disablingcolumns.
* Test disabled columns are removed from enabledcol setting and added to disabledcol setting.
*
* @covers ::enable_columns
* @covers ::disable_columns
* @return void
*/
public function test_enable_disable_columns(): void {
$neworder = $this->columnmanager->get_sorted_columns($this->columns);
shuffle($neworder);
set_columnbank_order::execute($neworder);
$currentconfig = get_config('qbank_columnsortorder', 'enabledcol');
$currentconfig = explode(',', $currentconfig);
$class = $currentconfig[array_rand($currentconfig, 1)];
$randomplugintodisable = explode('\\', $class)[0];
$olddisabledconfig = get_config('qbank_columnsortorder', 'disabledcol');
$this->columnmanager->disable_columns($randomplugintodisable);
$newdisabledconfig = get_config('qbank_columnsortorder', 'disabledcol');
$this->assertNotEquals($olddisabledconfig, $newdisabledconfig);
$this->columnmanager->enable_columns($randomplugintodisable);
$newdisabledconfig = get_config('qbank_columnsortorder', 'disabledcol');
$this->assertEmpty($newdisabledconfig);
$enabledconfig = get_config('qbank_columnsortorder', 'enabledcol');
$contains = strpos($enabledconfig, $randomplugintodisable);
$this->assertNotFalse($contains);
$this->assertIsInt($contains);
public function test_disable_columns(): void {
// Set up enabledcol with all plugins.
set_config('enabledcol', implode(',', $this->columns), 'qbank_columnsortorder');
$this->columnmanager = new column_manager();
$this->assertFalse(get_config('qbank_columnsortorder', 'disabledcol'));
// Disable a random plugin.
$plugincolumns = array_filter($this->columns, fn($column) => str_starts_with($column, 'qbank_'));
$randomcolumn = $plugincolumns[array_rand($plugincolumns, 1)];
$randomplugin = explode('\\', $randomcolumn)[0];
$this->columnmanager->disable_columns($randomplugin);
// The enabledcol setting should now contain all columns except the disabled plugin.
$expectedconfig = array_filter($this->columns, fn($column) => !str_starts_with($column, $randomplugin));
sort($expectedconfig);
$newconfig = explode(',', get_config('qbank_columnsortorder', 'enabledcol'));
sort($newconfig);
$this->assertEquals($expectedconfig, $newconfig);
$this->assertNotContains($randomcolumn, $newconfig);
// The disabledcol setting should only contain columns from the disabled plugin.
$disabledconfig = explode(',', get_config('qbank_columnsortorder', 'disabledcol'));
array_walk($disabledconfig, fn($column) => $this->assertStringStartsWith($randomplugin, $column));
}
/**
@@ -159,4 +238,68 @@ class column_manager_test extends advanced_testcase {
$this->assertNotFalse($contains);
$this->assertIsInt($contains);
}
/**
* Test enabled columns are removed from disabledcol setting and added to enabledcol setting.
*
* @return void
*/
public function test_enable_columns() {
// Set up disablecol with columns from 2 random plugins, and enabledcol with all other columns.
$plugincolumns = array_filter($this->columns, fn($column) => str_starts_with($column, 'qbank_'));
$plugins = array_unique(array_map(fn($column) => explode('\\', $column)[0], $plugincolumns));
$randomplugins = array_rand($plugins, 2);
$randomplugin1 = $plugins[$randomplugins[0]];
$randomplugin2 = $plugins[$randomplugins[1]];
$disabledcols = array_filter($this->columns,
fn($column) => str_starts_with($column, $randomplugin1) || str_starts_with($column, $randomplugin2));
$enabledcols = array_diff($this->columns, $disabledcols);
set_config('enabledcol', implode(',', $enabledcols), 'qbank_columnsortorder');
set_config('disabledcol', implode(',', $disabledcols), 'qbank_columnsortorder');
// Enable one of the disabled plugins.
$this->columnmanager = new column_manager();
$this->columnmanager->enable_columns($randomplugin1);
// The enabledcol setting should now contain all columns except the remaining disabled plugin.
$expectedenabled = array_filter($this->columns, fn($column) => !str_starts_with($column, $randomplugin2));
$expecteddisabled = array_filter($disabledcols, fn($column) => str_starts_with($column, $randomplugin2));
sort($expectedenabled);
sort($expecteddisabled);
$newenabled = explode(',', get_config('qbank_columnsortorder', 'enabledcol'));
sort($newenabled);
$this->assertEquals($expectedenabled, $newenabled);
$this->assertNotContains(reset($expecteddisabled), $newenabled);
// The disabledcol setting should only contain columns from the remaining disabled plugin.
$newdisabled = explode(',', get_config('qbank_columnsortorder', 'disabledcol'));
array_walk($newdisabled, fn($column) => $this->assertStringStartsWith($randomplugin2, $column));
}
/**
* Test that get_disabled_columns returns names of all the columns in the disabledcol setting
*
* @return void
*/
public function test_get_disabled_columns(): void {
// Set up disablecol with columns from 2 random plugins, and enabledcol with all other columns.
$plugincolumns = array_filter($this->columns, fn($column) => str_starts_with($column, 'qbank_'));
$randomcolumn = $plugincolumns[array_rand($plugincolumns, 1)];
$randomplugin = explode('\\', $randomcolumn)[0];
$disabledcols = array_filter($this->columns, fn($column) => str_starts_with($column, $randomplugin));
set_config('disabledcol', implode(',', $disabledcols), 'qbank_columnsortorder');
$this->columnmanager = new column_manager();
$expecteddisablednames = [];
foreach ($disabledcols as $disabledcol) {
$columnobject = new $disabledcol($this->questionbank);
$expecteddisablednames[] = (object) [
'disabledname' => $columnobject->get_title(),
];
}
$disablednames = $this->columnmanager->get_disabled_columns();
$this->assertEquals($expecteddisablednames, $disablednames);
}
}
@@ -1,60 +0,0 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace qbank_columnsortorder;
use advanced_testcase;
use qbank_columnsortorder\column_manager;
use qbank_columnsortorder\external\set_columnbank_order;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/question/classes/external.php');
/**
* Unit tests for qbank_columnsortorder external API.
*
* @package qbank_columnsortorder
* @author 2021, Ghaly Marc-Alexandre <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class columnsortorder_external_test extends advanced_testcase {
/**
* Test that external call core_question_external::set_columnbank_order($oldorder) sets proper
* data in config_plugins table.
*/
public function test_columnorder_external(): void {
$this->resetAfterTest(true);
$this->setAdminUser();
$columnsortorder = new column_manager();
$questionlistcolumns = $columnsortorder->get_columns();
$columnclasses = [];
foreach ($questionlistcolumns as $columnnobject) {
$columnclasses[] = $columnnobject->class;
}
shuffle($columnclasses);
set_columnbank_order::execute($columnclasses);
$currentconfig = (array)get_config('qbank_columnsortorder', 'enabledcol');
$currentconfig = explode(',', $currentconfig[0]);
$this->assertSame($columnclasses, $currentconfig);
}
}
+1 -1
View File
@@ -26,6 +26,6 @@
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'qbank_columnsortorder';
$plugin->version = 2023042401;
$plugin->version = 2023052200;
$plugin->requires = 2023041800;
$plugin->maturity = MATURITY_STABLE;
@@ -28,6 +28,7 @@
namespace qbank_editquestion;
use core_question\local\bank\question_version_status;
use qbank_editquestion\output\add_new_question;
/**
* Class editquestion_helper for methods related to add/edit/copy
@@ -69,25 +70,13 @@ class editquestion_helper {
* @param bool $canadd the text to display on the button.
* @param string $tooltip a tooltip to add to the button (optional).
* @param bool $disabled if true, the button will be disabled.
* @deprecated since Moodle 4.3. Use {@see add_new_question} renderable instead
* @todo Final deprecation in Moodle 4.7
*/
public static function create_new_question_button($categoryid, $params, $canadd, $tooltip = '', $disabled = false) {
global $PAGE, $OUTPUT;
$addquestiondisplay = array();
$addquestiondisplay['canadd'] = $canadd;
if ($canadd) {
$params['category'] = $categoryid;
$url = new \moodle_url('/question/bank/editquestion/addquestion.php', $params);
$buttonparams = ['disabled' => $disabled];
if (!empty($tooltip)) {
$buttonparams['title'] = $tooltip;
}
$addquestiondisplay['buttonhtml'] = $OUTPUT->single_button($url,
get_string('createnewquestion', 'question'),
'get', $buttonparams);
$addquestiondisplay['qtypeform'] = self::print_choose_qtype_to_add_form(array());
}
return $PAGE->get_renderer('qbank_editquestion')->render_create_new_question_button($addquestiondisplay);
global $OUTPUT;
debugging('create_new_question_button() is deprecated. Use the add_new_question renderable instead.');
return $OUTPUT->render(new add_new_question($categoryid, $params, $canadd));
}
/**
@@ -0,0 +1,67 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace qbank_editquestion\output;
use qbank_editquestion\editquestion_helper;
use renderer_base;
/**
* Create new question button
*
* @package qbank_editquestion
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class add_new_question implements \templatable, \renderable {
/** @var int $categoryid The ID of the category the quesiton will be added to. */
protected int $categoryid;
/** @var array $params URL parameters to pass to the add question form. */
protected array $params;
/** @var bool $canadd True if the add question button should be displayed. If false, a placeholder will be shown.*/
protected bool $canadd;
/**
* Store data for building the template context.
*
* @param int $categoryid
* @param array $params
* @param bool $canadd
*/
public function __construct(int $categoryid, array $params, bool $canadd) {
$this->categoryid = $categoryid;
$this->params = $params;
$this->canadd = $canadd;
}
public function export_for_template(renderer_base $output): array {
$addquestiondisplay = [];
$addquestiondisplay['canadd'] = $this->canadd;
if ($this->canadd) {
$this->params['category'] = $this->categoryid;
$url = new \moodle_url('/question/bank/editquestion/addquestion.php', $this->params);
$addquestiondisplay['buttonhtml'] = $output->single_button($url,
get_string('createnewquestion', 'question'),
'get');
$addquestiondisplay['qtypeform'] = editquestion_helper::print_choose_qtype_to_add_form(array());
}
return $addquestiondisplay;
}
}
@@ -52,8 +52,12 @@ class renderer extends \plugin_renderer_base {
*
* @param array $addquestiondata
* @return bool|string
* @deprecated since Moodle 4.3. Use {@see add_new_question} renderable instead
* @todo Final deprecation in Moodle 4.7
*/
public function render_create_new_question_button ($addquestiondata) {
debugging('render_create_new_question_button() is deprecated. '
. 'Pass the add_new_question renderable to render() instead.');
return $this->render_from_template('qbank_editquestion/add_new_question', $addquestiondata);
}
@@ -25,7 +25,9 @@
namespace qbank_editquestion;
use core\context;
use core_question\local\bank\view;
use qbank_editquestion\output\add_new_question;
/**
* Class columns is the entrypoint for the columns.
@@ -35,7 +37,7 @@ use core_question\local\bank\view;
* @author Safat Shahin <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class plugin_feature extends \core_question\local\bank\plugin_features_base{
class plugin_feature extends \core_question\local\bank\plugin_features_base {
public function get_question_columns($qbank): array {
return [
@@ -50,4 +52,23 @@ class plugin_feature extends \core_question\local\bank\plugin_features_base{
];
}
/**
* Return "Add new question" control.
*
* @param view $qbank The question bank view.
* @param context $context The current context, for permission checks.
* @param int $categoryid The current question category ID.
* @return \renderable[]
*/
public function get_question_bank_controls(view $qbank, context $context, int $categoryid): array {
if (!$qbank->allow_add_questions()) {
return [];
}
$canadd = has_capability('moodle/question:add', $context);
$urlparams = (new edit_action($qbank))->editquestionurl->params();
return [
100 => new add_new_question($categoryid, $urlparams, $canadd),
];
}
}
@@ -53,6 +53,14 @@ class discrimination_index extends column_base {
echo $PAGE->get_renderer('qbank_statistics')->render_discrimination_index($discriminationindex);
}
public function display_preview(\stdClass $question, string $rowclasses): void {
global $PAGE;
$this->display_start($question, $rowclasses);
echo $PAGE->get_renderer('qbank_statistics')->render_discrimination_index(50.00);
$this->display_end($question, $rowclasses);;
}
public function get_extra_classes(): array {
return ['pr-3'];
}
@@ -51,6 +51,14 @@ class discriminative_efficiency extends column_base {
echo $PAGE->get_renderer('qbank_statistics')->render_discriminative_efficiency($discriminativeefficiency);
}
public function display_preview(\stdClass $question, string $rowclasses): void {
global $PAGE;
$this->display_start($question, $rowclasses);
echo $PAGE->get_renderer('qbank_statistics')->render_discriminative_efficiency(25);
$this->display_end($question, $rowclasses);;
}
public function get_extra_classes(): array {
return ['pr-3'];
}
@@ -52,6 +52,14 @@ class facility_index extends column_base {
echo $PAGE->get_renderer('qbank_statistics')->render_facility_index($facility);
}
public function display_preview(\stdClass $question, string $rowclasses): void {
global $PAGE;
$this->display_start($question, $rowclasses);
echo $PAGE->get_renderer('qbank_statistics')->render_facility_index(0.75);
$this->display_end($question, $rowclasses);;
}
public function get_extra_classes(): array {
return ['pr-3'];
}
@@ -57,6 +57,26 @@ class checkbox_column extends column_base {
return get_string('selectquestionsforbulk', 'question');
}
public function display_header(array $columnactions = [], string $width = ''): void {
global $PAGE;
$renderer = $PAGE->get_renderer('core_question', 'bank');
$data = [];
$data['sortable'] = false;
$data['extraclasses'] = $this->get_classes();
$name = get_class($this);
$data['sorttip'] = true;
$data['tiptitle'] = $this->get_title();
$data['tip'] = $this->get_title_tip();
$data['colname'] = $this->get_column_name();
$data['name'] = get_string('selectall');
$data['class'] = $name;
$data['width'] = $width;
echo $renderer->render_column_header($data);
}
protected function display_content($question, $rowclasses): void {
global $OUTPUT;
@@ -0,0 +1,46 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_question\local\bank;
/**
* Base class to implement actions that can be performed on any column.
*
* A plugin should define subclasses of this for each action it provides, and return an instance of each from
* plugin_feature::get_column_actions(). The action returned from {@see get_action_menu_link()} will be displayed in each column
* header.
*
* @package core_question
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class column_action_base extends view_component {
/**
* A chance for subclasses to initialise themselves, for example to load lang strings,
* without having to override the constructor.
*/
protected function init(): void {
}
/**
* Return the action menu link for this action on the supplied column.
*
* @param column_base $column The column we are providing the action for.
* @return ?\action_menu_link The action to display in the column header.
*/
abstract public function get_action_menu_link(column_base $column): ?\action_menu_link;
}
+32 -2
View File
@@ -41,6 +41,9 @@ abstract class column_base extends view_component {
/** @var bool determine whether the column is td or th. */
protected $isheading = false;
/** @var bool determine whether the column is visible */
public $isvisible = true;
/**
* Set the column as heading
*/
@@ -78,8 +81,10 @@ abstract class column_base extends view_component {
/**
* Output the column header cell.
*
* @params column_action_base[] A list of column actions to include in the header.
*/
public function display_header(): void {
public function display_header(array $columnactions = [], string $width = ''): void {
global $PAGE;
$renderer = $PAGE->get_renderer('core_question', 'bank');
@@ -115,6 +120,16 @@ abstract class column_base extends view_component {
$data['help'] = $help->export_for_template($renderer);
}
$data['colname'] = $this->get_column_name();
$data['name'] = $title;
$data['class'] = $name;
$data['width'] = $width;
if (!empty($columnactions)) {
$actions = array_map(fn($columnaction) => $columnaction->get_action_menu_link($this), $columnactions);
$actionmenu = new \action_menu($actions);
$data['actionmenu'] = $actionmenu->export_for_template($renderer);
}
echo $renderer->render_column_header($data);
}
@@ -216,7 +231,10 @@ abstract class column_base extends view_component {
*/
protected function display_start($question, $rowclasses): void {
$tag = 'td';
$attr = ['class' => $this->get_classes()];
$attr = [
'class' => $this->get_classes(),
'data-pluginname' => get_class($this),
];
if ($this->isheading) {
$tag = 'th';
$attr['scope'] = 'row';
@@ -395,4 +413,16 @@ abstract class column_base extends view_component {
}
}
/**
* Output the column with an example value.
*
* By default, this will call $this->display() using whatever dummy data is passed in. Columns can override this
* to provide example output without requiring valid data.
*
* @param \stdClass $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_preview(\stdClass $question, string $rowclasses): void {
$this->display($question, $rowclasses);
}
}
@@ -0,0 +1,74 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_question\local\bank;
/**
* Default column manager class
*
* This class defines stub methods that can be overridden by a plugin defining its own column manager.
*
* @package core_question
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class column_manager_base {
/**
* Sort the list of columns
*
* Sort the provided list of columns into the order implemented in this column manager.
*
* @param array $unsortedcolumns Unordered array of columns
* @return array Columns in the desired order.
*/
public function get_sorted_columns(array $unsortedcolumns): array {
return $unsortedcolumns;
}
/**
* Given an array of columns, set the isvisible attribute.
*
* This base class leave all columns visible.
*
* @param column_base[] $columns
* @return array
*/
public function set_columns_visibility(array $columns): array {
return $columns;
}
/**
* Return a list of actions to display in an action menu for each column.
*
* @param view $qbank Question bank view.
* @return column_action_base[] A list of column actions.
*/
public function get_column_actions(view $qbank): array {
return [];
}
/**
* Given a column, return a value for its width CSS property.
*
* @param column_base $column
* @return string CSS width property value.
*/
public function get_column_width(column_base $column): string {
return '';
}
}
@@ -27,6 +27,11 @@
namespace core_question\local\bank;
use core\context;
use qbank_columnsortorder\local\qbank\column_action_move;
use qbank_columnsortorder\local\qbank\column_action_remove;
use qbank_columnsortorder\local\qbank\column_action_resize;
/**
* Class plugin_features_base is the base class for qbank plugins.
*
@@ -75,6 +80,31 @@ class plugin_features_base {
return [];
}
/**
* This method will return a column manager object, if this plugin provides one.
*
* @return ?column_manager_base
*/
public function get_column_manager(): ?column_manager_base {
return null;
}
/**
* This method will return an array of renderable objects, for adding additional controls to the question bank screen.
*
* The array returned can include a numeric index for each object, to indicate the position in which it should be displayed
* relative to other controls. If two plugins return controls with the same position, they will be displayed after one another,
* based on the alphabetical order of the plugin component names.
*
* @param view $qbank The question bank view.
* @param context $context The current context, for permission checks.
* @param int $categoryid The current question category ID.
* @return \renderable[]
*/
public function get_question_bank_controls(view $qbank, context $context, int $categoryid): array {
return [];
}
/**
* Return search conditions for the plugin.
*
+106 -46
View File
@@ -34,7 +34,7 @@ use core_plugin_manager;
use core_question\local\bank\condition;
use core_question\local\statistics\statistics_bulk_loader;
use core_question\output\question_bank_filter_ui;
use qbank_columnsortorder\column_manager;
use core_question\local\bank\column_manager_base;
use qbank_deletequestion\hidden_condition;
use qbank_editquestion\editquestion_helper;
use qbank_managecategories\category_condition;
@@ -193,7 +193,7 @@ class view {
protected $pagevars = [];
/**
* @var array $plugins all the qbank plugin objects.
* @var plugin_features_base[] $plugins Plugin feature objects for all enabled qbank plugins.
*/
protected $plugins = [];
@@ -212,6 +212,11 @@ class view {
*/
public $extraparams = [];
/**
* @var column_manager_base $columnmanager The column manager, can be overridden by plugins.
*/
protected $columnmanager;
/**
* Constructor for view.
*
@@ -266,8 +271,9 @@ class view {
$this->lastchangedid = clean_param($pageurl->param('lastchanged'), PARAM_INT);
$this->init_plugins();
$this->init_column_manager();
// Possibly the heading part can be removed.
$this->plugins = \core_component::get_plugin_list_with_class('qbank', 'plugin_feature', 'plugin_feature.php');
$this->set_pagevars($params);
$this->init_columns($this->wanted_columns(), $this->heading_column());
$this->init_question_actions();
@@ -276,17 +282,45 @@ class view {
}
/**
* Initialize bulk actions.
* Get an array of plugin features objects for all enabled qbank plugins.
*
* @return void
*/
protected function init_bulk_actions(): void {
protected function init_plugins(): void {
$plugins = \core_component::get_plugin_list_with_class('qbank', 'plugin_feature', 'plugin_feature.php');
foreach ($plugins as $componentname => $plugin) {
foreach ($plugins as $componentname => $pluginclass) {
if (!\core\plugininfo\qbank::is_plugin_enabled($componentname)) {
continue;
}
$this->plugins[$componentname] = new $pluginclass();
}
// Sort plugin list by component name.
ksort($this->plugins);
}
$pluginentrypoint = new $plugin();
$bulkactions = $pluginentrypoint->get_bulk_actions();
/**
* Allow qbank plugins to override the column manager.
*
* If multiple qbank plugins define a column manager, this will pick the first one sorted alphabetically.
*
* @return void
*/
protected function init_column_manager(): void {
$this->columnmanager = new column_manager_base();
foreach ($this->plugins as $plugin) {
if ($columnmanager = $plugin->get_column_manager()) {
$this->columnmanager = $columnmanager;
break;
}
}
}
/**
* Initialize bulk actions.
*/
protected function init_bulk_actions(): void {
foreach ($this->plugins as $componentname => $plugin) {
$bulkactions = $plugin->get_bulk_actions();
if (!is_array($bulkactions)) {
debugging("The method {$componentname}::get_bulk_actions() must return an " .
"array of bulk actions instead of a single bulk action. " .
@@ -302,7 +336,6 @@ class view {
'capabilities' => $bulkactionobject->get_bulk_action_capabilities()
];
}
}
}
@@ -336,14 +369,9 @@ class view {
* @return void
*/
protected function init_question_actions(): void {
$plugins = \core_component::get_plugin_list_with_class('qbank', 'plugin_feature', 'plugin_feature.php');
$this->questionactions = [];
foreach ($plugins as $component => $plugin) {
if (!qbank::is_plugin_enabled($component)) {
continue;
}
$pluginentrypointobject = new $plugin();
$menuactions = $pluginentrypointobject->get_question_actions($this);
foreach ($this->plugins as $plugin) {
$menuactions = $plugin->get_question_actions($this);
foreach ($menuactions as $menuaction) {
$this->questionactions[$menuaction::class] = $menuaction;
}
@@ -395,14 +423,8 @@ class view {
$questionbankclasscolumns = $this->get_class_for_columns();
$plugins = $this->plugins;
foreach ($plugins as $componentname => $plugin) {
$pluginentrypointobject = new $plugin();
$plugincolumnobjects = $pluginentrypointobject->get_question_columns($this);
// Don't need the plugins without column objects.
if (empty($plugincolumnobjects)) {
unset($plugins[$componentname]);
continue;
}
foreach ($this->plugins as $componentname => $plugin) {
$plugincolumnobjects = $plugin->get_question_columns($this);
foreach ($plugincolumnobjects as $columnobject) {
$columnname = $columnobject->get_column_name();
foreach ($this->corequestionbankcolumns as $key => $corequestionbankcolumn) {
@@ -431,15 +453,12 @@ class view {
$questionbankclasscolumns[$key] = $newpluginclasscolumn;
}
// Check if qbank_columnsortorder is enabled.
if (array_key_exists('columnsortorder', core_plugin_manager::instance()->get_enabled_plugins('qbank'))) {
$columnorder = new column_manager();
$questionbankclasscolumns = $columnorder->get_sorted_columns($questionbankclasscolumns);
}
$questionbankclasscolumns = $this->columnmanager->get_sorted_columns($questionbankclasscolumns);
$questionbankclasscolumns = $this->columnmanager->set_columns_visibility($questionbankclasscolumns);
// Mitigate the error in case of any regression.
foreach ($questionbankclasscolumns as $shortname => $questionbankclasscolumn) {
if (!is_object($questionbankclasscolumn)) {
if (!is_object($questionbankclasscolumn) || !$questionbankclasscolumn->isvisible) {
unset($questionbankclasscolumns[$shortname]);
}
}
@@ -456,7 +475,7 @@ class view {
$this->requiredcolumns = [];
$questionbankcolumns = $this->get_question_bank_plugins();
foreach ($questionbankcolumns as $classobject) {
if (empty($classobject)) {
if (empty($classobject) || !($classobject instanceof \core_question\local\bank\column_base)) {
continue;
}
$this->requiredcolumns[$classobject->get_column_name()] = $classobject;
@@ -500,7 +519,10 @@ class view {
if ($column->is_extra_row()) {
$this->extrarows[$column->get_column_name()] = $column;
} else {
$this->visiblecolumns[$column->get_column_name()] = $column;
// Only add columns which are visible.
if ($column->isvisible) {
$this->visiblecolumns[$column->get_column_name()] = $column;
}
}
}
@@ -905,7 +927,14 @@ class view {
* Shows the question bank interface.
*/
public function display(): void {
echo \html_writer::start_div('questionbankwindow boxwidthwide boxaligncenter');
$editcontexts = $this->contexts->having_one_edit_tab_cap('questions');
echo \html_writer::start_div('questionbankwindow boxwidthwide boxaligncenter', [
'data-component' => 'core_question',
'data-callback' => 'display_question_bank',
'data-contextid' => $editcontexts[array_key_last($editcontexts)]->id,
]);
// Show the filters and search options.
$this->wanted_filters();
// Continues with list of questions.
@@ -1087,22 +1116,50 @@ class view {
}
/**
* Create a new question form.
* Does the current view allow adding new questions?
*
* @param false|mixed|\stdClass $category
* @param bool $canadd
* @return bool True if the view supports adding new questions.
*/
protected function create_new_question_form($category, $canadd): void {
if (\core\plugininfo\qbank::is_plugin_enabled('qbank_editquestion')) {
echo editquestion_helper::create_new_question_button($category->id,
$this->questionactions['qbank_editquestion\edit_action']->editquestionurl->params(), $canadd);
public function allow_add_questions(): bool {
return true;
}
/**
* Output the question bank controls for each plugin.
*
* Controls will be output in the order defined by the array keys returned from
* {@see plugin_features_base::get_question_bank_controls}. If more than one plugin defines a control in the same position,
* they will placed after one another based on the alphabetical order of the plugins.
*
* @param \core\context $context The current context, for permissions checks.
* @param int $categoryid The current question category.
*/
protected function get_plugin_controls(\core\context $context, int $categoryid): string {
global $OUTPUT;
$orderedcontrols = [];
foreach ($this->plugins as $plugin) {
$plugincontrols = $plugin->get_question_bank_controls($this, $context, $categoryid);
foreach ($plugincontrols as $position => $plugincontrol) {
if (!array_key_exists($position, $orderedcontrols)) {
$orderedcontrols[$position] = [];
}
$orderedcontrols[$position][] = $plugincontrol;
}
}
ksort($orderedcontrols);
$output = '';
foreach ($orderedcontrols as $controls) {
foreach ($controls as $control) {
$output .= $OUTPUT->render($control);
}
}
return $OUTPUT->render_from_template('core_question/question_bank_controls', ['controls' => $output]);
}
/**
* Prints the table of questions in a category with interactions
*/
protected function display_question_list(): void {
public function display_question_list(): void {
// This function can be moderately slow with large question counts and may time out.
// We probably do not want to raise it to unlimited, so randomly picking 5 minutes.
// Note: We do not call this in the loop because quiz ob_ captures this function (see raise() PHP doc).
@@ -1120,6 +1177,7 @@ class view {
'data-contextid' => $this->get_most_specific_context()->id,
]
);
echo $this->get_plugin_controls($catcontext, $categoryid);
$this->build_query();
$questionsrs = $this->load_page_questions();
@@ -1376,13 +1434,13 @@ class view {
// Start of the table.
echo \html_writer::start_tag('table', [
'id' => 'categoryquestions',
'class' => 'table-responsive',
'data-defaultsort' => json_encode($this->sort)
'class' => 'question-bank-table generaltable table-responsive',
'data-defaultsort' => json_encode($this->sort),
]);
// Prints the table header.
echo \html_writer::start_tag('thead');
echo \html_writer::start_tag('tr');
echo \html_writer::start_tag('tr', ['class' => 'qbank-column-list']);
$this->print_table_headers();
echo \html_writer::end_tag('tr');
echo \html_writer::end_tag('thead');
@@ -1432,9 +1490,11 @@ class view {
/**
* Print table headers from child classes.
*/
public function print_table_headers(): void {
protected function print_table_headers(): void {
$columnactions = $this->columnmanager->get_column_actions($this);
foreach ($this->visiblecolumns as $column) {
$column->display_header();
$width = $this->columnmanager->get_column_width($column);
$column->display_header($columnactions, $width);
}
}
+25 -19
View File
@@ -43,31 +43,37 @@
Example context (json):
{
"sortable": false,
"extraclasses": "checkbox pr-3",
"tiptitle": "<input id=\"qbheadercheckbox\" name=\"qbheadercheckbox\" type=\"checkbox\" value=\"1\"\n data-action=\"toggle\"\n data-toggle=\"master\"\n data-togglegroup=\"qbank\"\n data-toggle-selectall=\"Select all\"\n data-toggle-deselectall=\"Deselect all\"\n \n\/>\n <label for=\"qbheadercheckbox\" class=\"accesshide\">Select all<\/label>",
"extraclasses": "checkbox pr-1",
"tiptitle": "<input id=\"qbheadercheckbox\" name=\"qbheadercheckbox\" type=\"checkbox\" value=\"1\" data-action=\"toggle\" data-toggle=\"master\" data-togglegroup=\"qbank\" data-toggle-selectall=\"Select all\" data-toggle-deselectall=\"Deselect all\"><label for=\"qbheadercheckbox\" class=\"accesshide\">Select all<\/label>",
"sorttip": true,
"tip": "Select questions for bulk actions"
"tip": "Select questions for bulk actions",
"class": "plugin_class",
"name": "plugin_name"
}
}}
<th class="header {{extraclasses}}" scope="col">
{{#title}}
<div class="title">{{title}}</div>
{{/title}}
{{^sortable}}
{{^sorttip}}
{{tiptitle}}
{{/sorttip}}
{{#sorttip}}
<span title="{{tip}}">{{{tiptitle}}}</span>
{{/sorttip}}
{{/sortable}}
<th class="header align-top {{extraclasses}}" scope="col" data-pluginname="{{class}}" data-name="{{name}}" {{#width}}style="width: {{width}};"{{/width}}>
<div class="header-container">
<div class="header-text">
{{#title}}
<div class="title mr-1">{{title}}</div>
{{/title}}
{{^sortable}}
{{^sorttip}}
<span class="mr-1">{{tiptitle}}</span>
{{/sorttip}}
{{#sorttip}}
<span class="mr-1" title="{{tip}}">{{{tiptitle}}}</span>
{{/sorttip}}
{{/sortable}}
{{#help}}
{{>core/help_icon}}
{{/help}}
{{#actionmenu}}{{>core/action_menu}}{{/actionmenu}}
</div>
</div>
{{#sortable}}
<div class="sorters">
{{{sortlinks}}}
</div>
{{/sortable}}
{{#help}}
{{>core/help_icon}}
{{/help}}
</th>
@@ -0,0 +1,32 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core_question/question_bank_controls
Flex container for displaying plugin controls at the top of the question bank table.
Context variables required for this template:
* controls - Rendered HTML for controls.
Example context (json):
{
"controls": "<div><button class=\"btn btn-secondary\">Control 1</button><button class=\"btn btn-secondary\">Control 2</button></div>"
}
}}
<div class="d-grid gap-2 d-md-flex justify-content-start mb-2">
{{{controls}}}
</div>
+5 -4
View File
@@ -1,4 +1,4 @@
@core @core_question
@core @core_question @javascript
Feature: A teacher can edit questions in the question bank
In order to improve my questions
As a teacher
@@ -22,6 +22,7 @@ Feature: A teacher can edit questions in the question bank
| Test questions | essay | Test question to be edited | Write about whatever you want |
And I am on the "Course 1" "core_question > course question bank" page logged in as "teacher1"
@javascript
Scenario: Edit a previously created question
When I am on the "Test question to be edited" "core_question > edit" page logged in as "teacher1"
And I set the following fields to these values:
@@ -30,7 +31,7 @@ Feature: A teacher can edit questions in the question bank
And I press "id_submitbutton"
Then I should see "Edited question name"
And I should not see "Test question to be edited"
And "Edited question name" row "Created by" column of "categoryquestions" table should contain "Teacher 1"
And I should see "Teacher 1"
Scenario: Edit a previously created question without permission 'moodle/question:moveall' and 'moodle/question:movemine'
Given I log in as "admin"
@@ -45,7 +46,7 @@ Feature: A teacher can edit questions in the question bank
And I press "id_submitbutton"
Then I should see "Edited question name"
And I should not see "Test question to be edited"
And "Edited question name" row "Created by" column of "categoryquestions" table should contain "Teacher 1"
And I should see "Teacher 1"
Scenario: Edit a previously created question without permission 'moodle/question:editall' and 'moodle/question:editmine'
Given I log in as "admin"
@@ -65,7 +66,7 @@ Feature: A teacher can edit questions in the question bank
And I set the field "Question name" to "Edited question name"
And I press "Cancel"
Then I should see "Test question to be edited"
And "Test question to be edited" row "Created by" column of "categoryquestions" table should contain "Admin User"
And I should see "Admin User"
Scenario: A question can have its idnumber removed
Given the following "questions" exist:
+9
View File
@@ -33,6 +33,15 @@ This files describes API changes for code that uses the question API.
`set_action_button()` on the `qbank_action_menu` object. This means plugin pages can include their own button like the
category management page has "Add category".
8) The question bank view now has additional hooks for qbank plugins to modify how the question bank is displayed.
`\core_question\local\bank\column_manager_base` defines methods for setting the order, visibility and width of columns, and
for providing a list of actions to be displayed in each column's header. Any qbank plugin can return a subclass of this from
its `plugin_features::get_column_manager()` method to implement its own logic for these methods. Only one column manager can be
active at a time, so in the case of multiple enabled plugins providing a column manager, the first one will be used by component
name sorted alphabetically.
Plugins can also define `plugin_features::get_question_bank_controls()` returning a list of renderables that will be displayed
above the question bank table, to provide additional functionality on the question bank page.
=== 4.2 ===
1) The question/qengine.js has been deprecated. We create core_question/question_engine