MDL-75125 core_question: Add action as a feature

This commit will implement question bank actions as a feature.
Previously, it used to implement columns and actions together in one
feature.
With this change it will have actions a different feature which will
allow
developers to implement actions in an easier way.

The existing "action column" API will be deprecated in a separate
commit.

Co-authored-by: Mark Johnson <[email protected]>
This commit is contained in:
Safat Shahin
2023-06-02 11:14:23 +01:00
committed by Mark Johnson
co-authored by Mark Johnson
parent 4ed782dd03
commit be1bc85028
7 changed files with 270 additions and 96 deletions
+1 -37
View File
@@ -31,7 +31,7 @@ namespace core_question\local\bank;
* @author 2021 Safat Shahin <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class column_base {
abstract class column_base extends view_component {
/**
* @var view $qbank the question bank view we are helping to render.
@@ -41,22 +41,6 @@ abstract class column_base {
/** @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
*/
@@ -297,30 +281,10 @@ abstract class column_base {
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 [];
}
@@ -24,12 +24,14 @@
namespace core_question\local\bank;
use \core\plugininfo\qbank;
/**
* 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
* {@see menu_action_column_base} interface and replace them with a single
* column containing an Edit menu.
*
* @copyright 2019 The Open University
@@ -37,33 +39,6 @@ namespace core_question\local\bank;
* @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.
*/
public function claim_menuable_columns($allcolumns): array {
$remainingcolumns = [];
foreach ($allcolumns as $key => $column) {
if ($column instanceof menuable_action) {
$this->actions[$key] = $column;
} else {
$remainingcolumns[$key] = $column;
}
}
return $remainingcolumns;
}
public function get_title() {
return get_string('actions');
}
@@ -74,11 +49,12 @@ class edit_menu_column extends column_base {
protected function display_content($question, $rowclasses): void {
global $OUTPUT;
$actions = $this->qbank->get_question_actions();
$menu = new \action_menu();
$menu->set_menu_trigger(get_string('edit'));
foreach ($this->actions as $actioncolumn) {
$action = $actioncolumn->get_action_menu_link($question);
foreach ($actions as $action) {
$action = $action->get_action_menu_link($question);
if ($action) {
$menu->add($action);
}
@@ -100,7 +76,7 @@ class edit_menu_column extends column_base {
/**
* Get menuable actions.
*
* @return menuable_action Menuable actions.
* @return menu_action_column_base Menuable actions.
*/
public function get_actions(): array {
return $this->actions;
@@ -38,7 +38,7 @@ namespace core_question\local\bank;
class plugin_features_base {
/**
* This method will return the array of objects to be rendered as a part of question bank columns/actions.
* This method will return the array of objects to be rendered as a part of question bank columns.
*
* @param view $qbank
* @return array
@@ -47,6 +47,16 @@ class plugin_features_base {
return [];
}
/**
* This method will return the array of objects to be rendered as a part of question bank actions.
*
* @param view $qbank
* @return question_action_base[]
*/
public function get_question_actions(view $qbank): array {
return [];
}
/**
* This method will return the object for the navigation node.
*
@@ -0,0 +1,77 @@
<?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/>.
/**
* 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 <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class question_action_base extends view_component {
/**
* Get the information required to display this action either as a menu item or a separate action column.
*
* For most actions, it should be sufficient to override just this method. {@see get_action_menu_link()} is the public interface
* and handles building a renderable menu link object from this data.
*
* 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)
*/
protected function get_url_icon_and_label(\stdClass $question): array {
return [null, null, null];
}
/**
* Return the action menu link for this action on the supplied question.
*
* For most actions, you will just need to override {@see get_url_icon_and_label()}. You only need to override
* this method if you need to pass additional attributes to {@see action_menu_link_secondary}, or use a different class to
* render the link.
*
* @param \stdClass $question
* @return \action_menu_link|null
*/
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);
}
}
+102 -26
View File
@@ -24,6 +24,7 @@
namespace core_question\local\bank;
use core\plugininfo\qbank;
use core_plugin_manager;
use core_question\bank\search\condition;
use core_question\local\statistics\statistics_bulk_loader;
@@ -96,6 +97,13 @@ class view {
*/
protected $requiredcolumns;
/**
* @var menu_action_base[] these are all the menu actions that can be displayed in a row's action menu.
*
* Array keys are the class name.
*/
protected $menuactions;
/**
* @var column_base[] these are the 'columns' that are
* actually displayed as a column, in order. Array keys are the class name.
@@ -201,6 +209,7 @@ class view {
// Possibly the heading part can be removed.
$this->init_columns($this->wanted_columns(), $this->heading_column());
$this->init_menu_actions();
$this->init_sort();
$this->init_search_conditions();
$this->init_bulk_actions();
@@ -251,6 +260,28 @@ class view {
}
}
/**
* Initialise list of menu actions for enabled question bank plugins.
*
* Menu action objects are stored in $this->menuactions, keyed by class name.
*
* @return void
*/
protected function init_menu_actions(): void {
$plugins = \core_component::get_plugin_list_with_class('qbank', 'plugin_feature', 'plugin_feature.php');
$this->menuactions = [];
foreach ($plugins as $component => $plugin) {
if (!qbank::is_plugin_enabled($component)) {
continue;
}
$pluginentrypointobject = new $plugin();
$menuactions = $pluginentrypointobject->get_question_actions($this);
foreach ($menuactions as $menuaction) {
$this->menuactions[$menuaction::class] = $menuaction;
}
}
}
/**
* Get the list of qbank plugins with available objects for features.
*
@@ -264,13 +295,6 @@ class view {
'question_type_column',
'question_name_idnumber_tags_column',
'edit_menu_column',
'edit_action_column',
'copy_action_column',
'tags_action_column',
'preview_action_column',
'history_action_column',
'delete_action_column',
'export_xml_action_column',
'question_status_column',
'version_number_column',
'creator_name_column',
@@ -388,14 +412,6 @@ class view {
* @param string $heading The name of column that is set as heading
*/
protected function init_columns($wanted, $heading = ''): void {
// If we are using the edit menu column, allow it to absorb all the actions.
foreach ($wanted as $column) {
if ($column instanceof edit_menu_column) {
$wanted = $column->claim_menuable_columns($wanted);
break;
}
}
// Now split columns into real columns and rows.
$this->visiblecolumns = [];
$this->extrarows = [];
@@ -589,26 +605,77 @@ class view {
}
/**
* Create the SQL query to retrieve the indicated questions, based on
* \core_question\bank\search\condition filters.
* Return an array 'table_alias' => 'JOIN clause' to bring in any data that
* the core view requires.
*
* @return string[] 'table_alias' => 'JOIN clause'
*/
protected function build_query(): void {
// Get the required tables and fields.
$joins = [];
$fields = ['qv.status', 'qc.id as categoryid', 'qv.version', 'qv.id as versionid', 'qbe.id as questionbankentryid'];
if (!empty($this->requiredcolumns)) {
foreach ($this->requiredcolumns as $column) {
$extrajoins = $column->get_extra_joins();
protected function get_required_joins(): array {
return [
'qv' => 'JOIN {question_versions} qv ON qv.questionid = q.id',
'qbe' => 'JOIN {question_bank_entries} qbe on qbe.id = qv.questionbankentryid',
'qc' => 'JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid',
];
}
/**
* Return an array of fields for any data that the core view requires.
*
* Use table alias 'q' for the question table, or one of the ones from get_required_joins.
* Every field requested must specify a table prefix.
*
* @return string[] fields required.
*/
protected function get_required_fields(): array {
return [
'q.id',
'q.qtype',
'q.createdby',
'qc.id as categoryid',
'qc.contextid',
'qv.status',
'qv.version',
'qv.id as versionid',
'qbe.id as questionbankentryid',
];
}
/**
* Gather query requirements from view component objects.
*
* This will take the required fields and joins for this view, and combine them with those for all active view components.
* Fields will be de-duplicated in multiple components require the same field.
* Joins will be de-duplicated if the alias and join clause match exactly.
*
* @throws \coding_exception If two components attempt to use the same alias for different joins.
* @param view_component[] $viewcomponents List of component objects included in the current view
* @return array [$fields, $joins] SQL fields and joins to add to the query.
*/
protected function get_component_requirements(array $viewcomponents): array {
$fields = $this->get_required_fields();
$joins = $this->get_required_joins();
if (!empty($viewcomponents)) {
foreach ($viewcomponents as $viewcomponent) {
$extrajoins = $viewcomponent->get_extra_joins();
foreach ($extrajoins as $prefix => $join) {
if (isset($joins[$prefix]) && $joins[$prefix] != $join) {
throw new \coding_exception('Join ' . $join . ' conflicts with previous join ' . $joins[$prefix]);
}
$joins[$prefix] = $join;
}
$fields = array_merge($fields, $column->get_required_fields());
$fields = array_merge($fields, $viewcomponent->get_required_fields());
}
}
$fields = array_unique($fields);
return [array_unique($fields), $joins];
}
/**
* Create the SQL query to retrieve the indicated questions, based on
* \core_question\bank\search\condition filters.
*/
protected function build_query(): void {
// Get the required tables and fields.
[$fields, $joins] = $this->get_component_requirements(array_merge($this->requiredcolumns, $this->questionactions));
// Build the order by clause.
$sorts = [];
@@ -1335,4 +1402,13 @@ class view {
public function is_listing_specific_versions(): bool {
return false;
}
/**
* Return array of menu actions.
*
* @return menu_action_base[]
*/
public function get_menuactions(): array {
return $this->menuactions;
}
}
@@ -0,0 +1,72 @@
<?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;
/**
* Abstract class to define functionality shared by all pluggable components used in the question bank view.
*
* @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 view_component {
/**
* 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 {
}
/**
* Return an array 'table_alias' => 'JOIN clause' to bring in any data that
* this feature requires.
*
* The return values for all the features will be checked. It is OK if two
* features join in the same table with the same alias and identical JOIN clauses.
* If two features try to use the same alias with different joins, you get an error.
* Tables included by default are question (alias q) and those defined in {@see view::get_required_joins()}
*
* 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 string[] '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 string[] fields required.
*/
public function get_required_fields(): array {
return [];
}
}
-1
View File
@@ -256,7 +256,6 @@ abstract class question_edit_form extends question_wizard_form {
$this->definition_inner($mform);
if (core_tag_tag::is_enabled('core_question', 'question')
&& class_exists('qbank_tagquestion\\tags_action_column')
&& \core\plugininfo\qbank::is_plugin_enabled('qbank_tagquestion')) {
$this->add_tag_fields($mform);
}