MDL-83092 AI: Placement action needs to check status of provider action

When all providers for a certain action are disabled, or if providers
are enabled but all individual actions of a type are disabled, then
a warning is shown on the placement admin settings page for actions
that have no providers available.

Also the text editor placement will not show the option to generate
images or text if there are no actions available.
This commit is contained in:
Matt Porritt
2024-09-20 10:26:49 +08:00
committed by Jun Pataleta
parent f6141a67d8
commit cfe65f1772
7 changed files with 213 additions and 51 deletions
+32 -9
View File
@@ -63,7 +63,7 @@ class manager {
*
* Will return an array of arrays, indexed by action name.
*
* @param array $actions An array of action class names.
* @param array $actions An array of fully qualified action class names.
* @param bool $enabledonly If true, only return enabled providers.
* @return array An array of provider instances indexed by action name.
*/
@@ -233,15 +233,16 @@ class manager {
* Set the action state for a given plugin.
*
* @param string $plugin The name of the plugin.
* @param string $action The action to be set.
* @param string $actionbasename The action to be set.
* @param int $enabled The state to be set (e.g., enabled or disabled).
* @return bool Returns true if the configuration was successfully set, false otherwise.
*/
public static function set_action_state(string $plugin, string $action, int $enabled): bool {
$oldvalue = static::is_action_enabled($plugin, $action);
public static function set_action_state(string $plugin, string $actionbasename, int $enabled): bool {
$actionclass = 'core_ai\\aiactions\\' . $actionbasename;
$oldvalue = static::is_action_enabled($plugin, $actionclass);
// Only set value if there is no config setting or if the value is different from the previous one.
if ($oldvalue !== $enabled) {
set_config($action, $enabled, $plugin);
set_config($actionbasename, $enabled, $plugin);
add_to_config_log('disabled', !$oldvalue, !$enabled, $plugin);
\core_plugin_manager::reset_caches();
return true;
@@ -253,16 +254,38 @@ class manager {
* Check if an action is enabled for a given plugin.
*
* @param string $plugin The name of the plugin.
* @param string $action The action to be checked.
* @param string $actionclass The fully qualified action class name to be checked.
* @return mixed Returns the configuration value of the action for the given plugin.
*/
public static function is_action_enabled(string $plugin, string $action): bool {
$value = get_config($plugin, $action);
public static function is_action_enabled(string $plugin, string $actionclass): bool {
$value = get_config($plugin, $actionclass::get_basename());
// If not exist in DB, set it to true (enabled).
if ($value === false) {
return true;
}
return (bool) $value;
}
/**
* Check if an action is available.
* Action is available if it is enabled for at least one enabled provider.
*
* @param string $actionclass The fully qualified action class name to be checked.
* @return bool
*/
public static function is_action_available(string $actionclass): bool {
$providers = self::get_providers_for_actions([$actionclass], true);
// Check if the requested action is enabled for at least one provider.
foreach ($providers as $provideractions) {
foreach ($provideractions as $provider) {
$classnamearray = explode('\\', $provider::class);
$pluginname = reset($classnamearray);
if (self::is_action_enabled($pluginname, $actionclass)) {
return true;
}
}
}
// There are no providers with this action enabled.
return false;
}
}
@@ -16,6 +16,7 @@
namespace core_ai\table;
use core_ai\manager;
use core_table\dynamic as dynamic_table;
use flexible_table;
use moodle_url;
@@ -47,7 +48,7 @@ class aiplacement_action_management_table extends flexible_table implements dyna
$this->pluginname = $pluginname;
// Get the list of actions that this provider supports.
$this->actions = \core_ai\manager::get_supported_actions($this->pluginname);
$this->actions = manager::get_supported_actions($this->pluginname);
parent::__construct($this->get_table_id());
@@ -127,19 +128,20 @@ class aiplacement_action_management_table extends flexible_table implements dyna
protected function col_namedesc(stdClass $row): string {
global $OUTPUT;
$providerurl = new moodle_url('/admin/settings.php', ['section' => 'aiprovider']);
if (!$this->has_provider($row->action)) {
return $OUTPUT->render_from_template('core_ai/admin_noproviders', [
'providerurl' => $providerurl->out(),
]);
}
$params = [
'name' => $row->action::get_name(),
'description' => $row->action::get_description(),
];
$output = $OUTPUT->render_from_template('core_admin/table/namedesc', $params);
return $OUTPUT->render_from_template('core_admin/table/namedesc', $params);
if (!manager::is_action_available($row->action)) {
$providerurl = new moodle_url('/admin/settings.php', ['section' => 'aiprovider']);
$output .= $OUTPUT->render_from_template('core_ai/admin_noproviders', [
'providerurl' => $providerurl->out(),
]);
}
return $output;
}
/**
@@ -202,11 +204,11 @@ class aiplacement_action_management_table extends flexible_table implements dyna
* Print the table.
*/
public function out(): void {
foreach ($this->actions as $action) {
foreach ($this->actions as $actionclass) {
// Construct the row data.
$rowdata = (object) [
'action' => $action,
'enabled' => \core_ai\manager::is_action_enabled($this->pluginname, $action::get_basename()),
'action' => $actionclass,
'enabled' => manager::is_action_enabled($this->pluginname, $actionclass),
];
$this->add_data_keyed(
$this->format_row($rowdata),
@@ -228,19 +230,6 @@ class aiplacement_action_management_table extends flexible_table implements dyna
$this->define_baseurl($url);
}
/**
* Check if the action has any enabled providers.
*
* Returns True if the action has a provider.
*
* @param string $action The action to check.
* @return bool True if the action has a provider.
*/
private function has_provider(string $action): bool {
$providers = \core_ai\manager::get_providers_for_actions([$action], true);
return !empty($providers[$action]);
}
#[\Override]
public function has_capability(): bool {
return has_capability('moodle/site:config', $this->get_context());
@@ -214,11 +214,11 @@ class aiprovider_action_management_table extends flexible_table implements dynam
* Print the table.
*/
public function out(): void {
foreach ($this->actions as $action) {
foreach ($this->actions as $actionclass) {
// Construct the row data.
$rowdata = (object) [
'action' => $action,
'enabled' => \core_ai\manager::is_action_enabled($this->pluginname, $action::get_basename()),
'action' => $actionclass,
'enabled' => \core_ai\manager::is_action_enabled($this->pluginname, $actionclass),
];
$this->add_data_keyed(
$this->format_row($rowdata),
+60
View File
@@ -0,0 +1,60 @@
@core @core_admin @core_ai
Feature: An administrator can manage AI subsystem settings
In order to alter the user experience
As an admin
I can manage AI subsystem settings
@javascript
Scenario: An administrator can control the enabled state of AI Provider plugins using JavaScript
Given I am logged in as "admin"
And I navigate to "AI > Manage settings for AI providers" in site administration
When I toggle the "Enable OpenAI API Provider" admin switch "on"
And I should see "OpenAI API Provider enabled."
And I reload the page
And I should see "Disable OpenAI API Provider"
And I toggle the "Disable OpenAI API Provider" admin switch "off"
Then I should see "OpenAI API Provider disabled."
@javascript
Scenario: An administrator can control the enabled state of AI Placement plugins using JavaScript
Given I am logged in as "admin"
And I navigate to "AI > Manage settings for AI placements" in site administration
When I toggle the "Enable HTML Text Editor Placement" admin switch "on"
And I should see "HTML Text Editor Placement enabled."
And I reload the page
And I should see "Disable HTML Text Editor Placement"
And I toggle the "Disable HTML Text Editor Placement" admin switch "off"
Then I should see "HTML Text Editor Placement disabled."
@javascript
Scenario: Placement actions should be available when an Administrator enables AI Providers using JavaScript
Given I am logged in as "admin"
And I navigate to "AI > Manage settings for AI providers" in site administration
When I toggle the "Enable OpenAI API Provider" admin switch "on"
And I should see "OpenAI API Provider enabled."
And I navigate to "AI > Manage settings for AI placements" in site administration
And I click on the "Settings" link in the table row containing "HTML Text Editor Placement"
Then I should not see "This action is unavailable."
@javascript
Scenario: Placement actions should not be available when an Administrator disables AI Providers using JavaScript
Given I am logged in as "admin"
And I navigate to "AI > Manage settings for AI providers" in site administration
When I toggle the "Enable OpenAI API Provider" admin switch "off"
And I navigate to "AI > Manage settings for AI placements" in site administration
And I click on the "Settings" link in the table row containing "HTML Text Editor Placement"
And I should see "This action is unavailable." in the table row containing "Generate text"
Then I should see "This action is unavailable." in the table row containing "Generate image"
@javascript
Scenario: Placement actions should not be available for enabled Providers when an Administrator disables an Action using JavaScript
Given I am logged in as "admin"
And I navigate to "AI > Manage settings for AI providers" in site administration
When I toggle the "Enable OpenAI API Provider" admin switch "on"
And I should see "OpenAI API Provider enabled."
And I click on the "Settings" link in the table row containing "OpenAI API Provider"
And I toggle the "Generate text" admin switch "off"
And I navigate to "AI > Manage settings for AI placements" in site administration
And I click on the "Settings" link in the table row containing "HTML Text Editor Placement"
And I should see "This action is unavailable." in the table row containing "Generate text"
Then I should not see "This action is unavailable." in the table row containing "Generate image"
+32 -5
View File
@@ -91,8 +91,8 @@ final class manager_test extends \advanced_testcase {
$this->assertCount(1, $providers[generate_text::class]);
$this->assertCount(1, $providers[summarise_text::class]);
// Disable the generate text action for the open ai provider.
set_config(generate_text::class, 0, 'aiprovider_openai');
// Disable the generate text action for the Open AI provider.
manager::set_action_state('aiprovider_openai', generate_text::class::get_basename(), 0);
$providers = $manager->get_providers_for_actions($actions, true);
// Assert that there is no provider for the generate text action.
@@ -373,7 +373,7 @@ final class manager_test extends \advanced_testcase {
$this->assertTrue($result);
// Disable the action.
set_config($action, 0, $plugin);
manager::set_action_state($plugin, $action::get_basename(), 0);
// Should now be disabled.
$result = manager::is_action_enabled($plugin, $action);
@@ -389,14 +389,41 @@ final class manager_test extends \advanced_testcase {
$action = generate_image::class;
// Disable the action.
set_config($action, 0, $plugin);
set_config('generate_image', 0, $plugin);
// Should now be disabled.
$result = manager::is_action_enabled($plugin, $action);
$this->assertFalse($result);
// Enable the action.
$result = manager::set_action_state($plugin, $action, 1);
$result = manager::set_action_state($plugin, $action::get_basename(), 1);
$this->assertTrue($result);
}
/**
* Test is_action_available method.
*/
public function test_is_action_available(): void {
$this->resetAfterTest();
$action = generate_image::class;
// Plugin is disabled by default, action state should not matter. Everything should be false.
$result = manager::is_action_available($action);
$this->assertFalse($result);
// Enable the plugin, actions will be enabled by default when the plugin is enabled.
$manager = \core_plugin_manager::resolve_plugininfo_class('aiprovider');
$manager::enable_plugin('openai', 1);
// Should now be available.
$result = manager::is_action_available($action);
$this->assertTrue($result);
// Disable the action.
set_config('generate_image', 0, 'aiprovider_openai');
// Should now be unavailable.
$result = manager::is_action_available($action);
$this->assertFalse($result);
}
}
@@ -93,12 +93,10 @@ class plugininfo extends plugin implements plugin_with_buttons, plugin_with_menu
$manager = \core_plugin_manager::resolve_plugininfo_class($plugintype);
$allowedactions = [];
if ($manager::is_plugin_enabled($pluginname)) {
$providers = manager::get_providers_for_actions(array_values(self::$possibleactions), true);
foreach (self::$possibleactions as $action => $providerclass) {
foreach (self::$possibleactions as $action => $actionclass) {
if (
has_capability("aiplacement/editor:{$action}", $context)
&& manager::is_action_enabled('aiplacement_editor', $action)
&& !empty($providers[$providerclass])
&& manager::is_action_available($actionclass)
) {
if ($action == 'generate_image') {
// For generate image, we need to check if the user has the capability to upload files.
+70 -5
View File
@@ -134,7 +134,7 @@ class behat_general extends behat_base {
// Getting the refresh time and the url if present.
if (strstr($content, 'url') != false) {
list($waittime, $url) = explode(';', $content);
[$waittime, $url] = explode(';', $content);
// Cleaning the URL value.
$url = trim(substr($url, strpos($url, 'http')));
@@ -974,8 +974,8 @@ class behat_general extends behat_base {
$msg .= " in the '{$containerelement}' '{$containerselectortype}'";
}
list($preselector, $prelocator) = $this->transform_selector($preselectortype, $preelement);
list($postselector, $postlocator) = $this->transform_selector($postselectortype, $postelement);
[$preselector, $prelocator] = $this->transform_selector($preselectortype, $preelement);
[$postselector, $postlocator] = $this->transform_selector($postselectortype, $postelement);
$newlines = [
"\r\n",
@@ -1737,7 +1737,7 @@ EOF;
public function following_should_download_between_and_bytes($link, $minexpectedsize, $maxexpectedsize) {
// If the minimum is greater than the maximum then swap the values.
if ((int)$minexpectedsize > (int)$maxexpectedsize) {
list($minexpectedsize, $maxexpectedsize) = array($maxexpectedsize, $minexpectedsize);
[$minexpectedsize, $maxexpectedsize] = [$maxexpectedsize, $minexpectedsize];
}
$exception = new ExpectationException('Error while downloading data from ' . $link, $this->getSession());
@@ -2101,7 +2101,7 @@ EOF;
$validmodifiers = array('ctrl', 'alt', 'shift', 'meta');
$char = $key;
if (strpos($key, '-')) {
list($modifier, $char) = preg_split('/-/', $key, 2);
[$modifier, $char] = preg_split('/-/', $key, 2);
$modifier = strtolower($modifier);
if (!in_array($modifier, $validmodifiers)) {
throw new ExpectationException(sprintf('Unknown key modifier: %s.', $modifier),
@@ -2643,4 +2643,69 @@ EOF;
]);
$this->execute("behat_general::i_wait_to_be_redirected");
}
/**
* Clicks on a specific link within a table row.
* Good for clicking links on tables where links have repeated text in diiferent rows.
*
* Example:
* - I click on the "Settings" link in the row containing "HTML Text Editor Placement"
*
* @Given /^I click on the "(?P<linktext>(?:[^"]|\\")*)" link in the table row containing "(?P<rowtext>(?:[^"]|\\")*)"$/
* @param string $linktext
* @param string $rowtext
*/
public function i_click_on_the_link_in_the_table_row_containing(string $linktext, string $rowtext): void {
$row = $this->getSession()->getPage()->find('xpath', "//tr[contains(., '{$rowtext}')]");
if (!$row) {
throw new Exception("Row containing '{$rowtext}' not found");
}
$link = $row->findLink($linktext);
if (!$link) {
throw new Exception("Link '{$linktext}' not found in the row containing '{$rowtext}'");
}
$link->click();
}
/**
* Checks if a specific text is present in a table row.
* Good for checking text in tables where text is repeated in different rows.
*
* Example:
* - I should see "This action is unavailable." in the table row containing "Generate text"
*
* @Then /^I should see "(?P<text>(?:[^"]|\\")*)" in the table row containing "(?P<rowtext>(?:[^"]|\\")*)"$/
* @param string $text
* @param string $rowtext
*/
public function i_should_see_in_the_table_row_containing(string $text, string $rowtext): void {
$row = $this->getSession()->getPage()->find('xpath', "//tr[contains(., '{$rowtext}')]");
if (!$row) {
throw new Exception("Row containing '{$rowtext}' not found");
}
if (strpos($row->getText(), $text) === false) {
throw new Exception("Text '{$text}' not found in the row containing '{$rowtext}'");
}
}
/**
* Checks if a specific text is not present in a table row.
* Good for checking text in tables where text is repeated in different rows.
*
* Example:
* - I should not see "This action is unavailable." in the table row containing "Generate text"
*
* @Then /^I should not see "(?P<text>(?:[^"]|\\")*)" in the table row containing "(?P<rowtext>(?:[^"]|\\")*)"$/
* @param string $text
* @param string $rowtext
*/
public function i_should_not_see_in_the_table_row_containing(string $text, string $rowtext): void {
$row = $this->getSession()->getPage()->find('xpath', "//tr[contains(., '{$rowtext}')]");
if (!$row) {
throw new Exception("Row containing '{$rowtext}' not found");
}
if (strpos($row->getText(), $text) !== false) {
throw new Exception("Text '{$text}' found in the row containing '{$rowtext}'");
}
}
}