MDL-85738 ai: Add base support for AI access controls
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
issueNumber: MDL-85738
|
||||
notes:
|
||||
core_ai:
|
||||
- message: >
|
||||
- Added `get_enabled_actions_in_course_module` method in public/ai/classes/manager.php to get enabled AI actions in course module.
|
||||
- Added `is_ai_tools_enabled_in_course` method in public/ai/classes/manager.php to check if AI tools is enabled in course.
|
||||
- Added `is_action_enabled_in_context` method in public/ai/classes/manager.php to check if an action is enabled in a particular context.
|
||||
- Added `get_ai_fields_from_course_module` method in public/ai/classes/manager.php to get the AI related fields from the course module.
|
||||
- Added `is_html_editor_placement_available` method in public/ai/placement/editor/classes/utils.php to check if editor placement is enabled.
|
||||
- Added `get_actions_available` method in public/ai/placement/editor/classes/utils.php to get available actions for editor placement.
|
||||
type: improved
|
||||
@@ -28,6 +28,7 @@ use core\plugininfo\aiprovider as aiproviderplugin;
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class manager {
|
||||
|
||||
/**
|
||||
* Create a new AI manager.
|
||||
*
|
||||
@@ -338,6 +339,36 @@ class manager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an action is enabled in a particular context.
|
||||
*
|
||||
* @param \context $context The context to use.
|
||||
* @param string $actionclass The action class name to check.
|
||||
* @return bool Return true enabled and allowed.
|
||||
*/
|
||||
public function is_action_enabled_in_context(\context $context, string $actionclass): bool {
|
||||
// Only check if we are in a supported context.
|
||||
if (in_array($context->contextlevel, [CONTEXT_COURSE, CONTEXT_COURSECAT, CONTEXT_MODULE])) {
|
||||
// Return false if AI tools is not enabled at the course level.
|
||||
if (!self::is_ai_tools_enabled_in_course($context)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($context->contextlevel == CONTEXT_MODULE) {
|
||||
// Detect if this is a newly created module (doesn't have any AI settings yet).
|
||||
$record = self::get_ai_fields_from_course_module($context->instanceid);
|
||||
if (is_null($record->enabledaiactions)) {
|
||||
return true;
|
||||
}
|
||||
// Check if the action is one of our enabled ones.
|
||||
$enabledactions = self::get_enabled_actions_in_course_module($record);
|
||||
return in_array($actionclass, $enabledactions);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an action is available.
|
||||
* Action is available if it is enabled for at least one enabled provider.
|
||||
@@ -684,4 +715,69 @@ class manager {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the AI related fields from the course module.
|
||||
*
|
||||
* @param int $id The course module to check.
|
||||
* @return \stdClass Return AI related fields.
|
||||
*/
|
||||
public static function get_ai_fields_from_course_module(int $id): \stdClass {
|
||||
global $DB;
|
||||
|
||||
return $DB->get_record(
|
||||
table: 'course_modules',
|
||||
conditions: ['id' => $id],
|
||||
fields: 'enableaitools, enabledaiactions',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the enabled actions in a course module context.
|
||||
*
|
||||
* @param \stdClass $record AI related fields from course module.
|
||||
* @return array An array of enabled actions in the course module.
|
||||
*/
|
||||
public static function get_enabled_actions_in_course_module(\stdClass $record): array {
|
||||
$enabledaiactions = [];
|
||||
|
||||
if (is_null($record->enableaitools) || $record->enableaitools) {
|
||||
// Get AI action settings and determine which ones are enabled.
|
||||
if (!empty($record->enabledaiactions)) {
|
||||
$enabledaiactions = array_keys(
|
||||
array_filter((array) json_decode($record->enabledaiactions), function ($value): bool {
|
||||
return $value == 1;
|
||||
})
|
||||
);
|
||||
// Set to classname format.
|
||||
foreach ($enabledaiactions as $key => $action) {
|
||||
$enabledaiactions[$key] = "core_ai\\aiactions\\{$action}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $enabledaiactions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if AI tools are enabled in the course.
|
||||
*
|
||||
* @param \context $context The context to check.
|
||||
* @return bool True if AI tools are enabled in the course, false otherwise.
|
||||
*/
|
||||
public static function is_ai_tools_enabled_in_course(\context $context): bool {
|
||||
global $DB;
|
||||
|
||||
if (in_array($context->contextlevel, [CONTEXT_COURSE, CONTEXT_COURSECAT])) {
|
||||
$courseid = $context->instanceid;
|
||||
} else {
|
||||
$courseid = $DB->get_field('course_modules', 'course', ['id' => $context->instanceid]);
|
||||
}
|
||||
|
||||
$enableaitools = $DB->get_field('course', 'enableaitools', ['id' => $courseid]);
|
||||
if (!is_null($enableaitools) && !$enableaitools) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,3 +1,3 @@
|
||||
define("aiplacement_courseassist/selectors",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={ELEMENTS:{AIDRAWER:"#ai-drawer",AIDRAWER_BODY:"#ai-drawer .ai-drawer-body",PAGE:"#page",MAIN_REGION:'[role="main"]',AIDRAWER_CLOSE:"#ai-drawer-close",RESPONSE:".course-assist-response",JUMPTO:'.course-assist-controls [data-region="jumpto"]',ACTION:'.course-assist-controls [data-input-type="action"]'},ACTIONS:{SUMMARY:'.course-assist-controls [data-action="summarise"]',EXPLAIN:'.course-assist-controls [data-action="explain"]',RETRY:'.course-assist-controls [data-action="retry"]',DECLINE:'.ai-policy-block [data-action="decline"]',ACCEPT:'.ai-policy-block [data-action="accept"]',REGENERATE:'.course-assist-controls [data-action="regenerate"]',CANCEL:'.course-assist-controls [data-action="cancel"]'}},_exports.default}));
|
||||
define("aiplacement_courseassist/selectors",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={ELEMENTS:{AIDRAWER:"#ai-drawer",AIDRAWER_BODY:"#ai-drawer .ai-drawer-body",PAGE:"#page",MAIN_REGION:'[role="main"]',AIDRAWER_CLOSE:"#ai-drawer-close",RESPONSE:".course-assist-response",JUMPTO:'.course-assist-controls [data-region="jumpto"]',ACTION:'.course-assist-controls [data-input-type="action"]'},ACTIONS:{SUMMARY:'.course-assist-controls [data-action="summarise_text"]',EXPLAIN:'.course-assist-controls [data-action="explain_text"]',RETRY:'.course-assist-controls [data-action="retry"]',DECLINE:'.ai-policy-block [data-action="decline"]',ACCEPT:'.ai-policy-block [data-action="accept"]',REGENERATE:'.course-assist-controls [data-action="regenerate"]',CANCEL:'.course-assist-controls [data-action="cancel"]'}},_exports.default}));
|
||||
|
||||
//# sourceMappingURL=selectors.min.js.map
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"selectors.min.js","sources":["../src/selectors.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 * Define all of the selectors we will be using on the AI Course assistant.\n *\n * @module aiplacement_courseassist/selectors\n * @copyright 2024 Huong Nguyen <[email protected]>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default {\n ELEMENTS: {\n AIDRAWER: '#ai-drawer',\n AIDRAWER_BODY: '#ai-drawer .ai-drawer-body',\n PAGE: '#page',\n MAIN_REGION: '[role=\"main\"]',\n AIDRAWER_CLOSE: '#ai-drawer-close',\n RESPONSE: '.course-assist-response',\n JUMPTO: '.course-assist-controls [data-region=\"jumpto\"]',\n ACTION: '.course-assist-controls [data-input-type=\"action\"]',\n },\n ACTIONS: {\n SUMMARY: '.course-assist-controls [data-action=\"summarise\"]',\n EXPLAIN: '.course-assist-controls [data-action=\"explain\"]',\n RETRY: '.course-assist-controls [data-action=\"retry\"]',\n DECLINE: '.ai-policy-block [data-action=\"decline\"]',\n ACCEPT: '.ai-policy-block [data-action=\"accept\"]',\n REGENERATE: '.course-assist-controls [data-action=\"regenerate\"]',\n CANCEL: '.course-assist-controls [data-action=\"cancel\"]',\n }\n};\n"],"names":["ELEMENTS","AIDRAWER","AIDRAWER_BODY","PAGE","MAIN_REGION","AIDRAWER_CLOSE","RESPONSE","JUMPTO","ACTION","ACTIONS","SUMMARY","EXPLAIN","RETRY","DECLINE","ACCEPT","REGENERATE","CANCEL"],"mappings":"oLAsBe,CACXA,SAAU,CACNC,SAAU,aACVC,cAAe,6BACfC,KAAM,QACNC,YAAa,gBACbC,eAAgB,mBAChBC,SAAU,0BACVC,OAAQ,iDACRC,OAAQ,sDAEZC,QAAS,CACLC,QAAS,oDACTC,QAAS,kDACTC,MAAO,gDACPC,QAAS,2CACTC,OAAQ,0CACRC,WAAY,qDACZC,OAAQ"}
|
||||
{"version":3,"file":"selectors.min.js","sources":["../src/selectors.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 * Define all of the selectors we will be using on the AI Course assistant.\n *\n * @module aiplacement_courseassist/selectors\n * @copyright 2024 Huong Nguyen <[email protected]>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default {\n ELEMENTS: {\n AIDRAWER: '#ai-drawer',\n AIDRAWER_BODY: '#ai-drawer .ai-drawer-body',\n PAGE: '#page',\n MAIN_REGION: '[role=\"main\"]',\n AIDRAWER_CLOSE: '#ai-drawer-close',\n RESPONSE: '.course-assist-response',\n JUMPTO: '.course-assist-controls [data-region=\"jumpto\"]',\n ACTION: '.course-assist-controls [data-input-type=\"action\"]',\n },\n ACTIONS: {\n SUMMARY: '.course-assist-controls [data-action=\"summarise_text\"]',\n EXPLAIN: '.course-assist-controls [data-action=\"explain_text\"]',\n RETRY: '.course-assist-controls [data-action=\"retry\"]',\n DECLINE: '.ai-policy-block [data-action=\"decline\"]',\n ACCEPT: '.ai-policy-block [data-action=\"accept\"]',\n REGENERATE: '.course-assist-controls [data-action=\"regenerate\"]',\n CANCEL: '.course-assist-controls [data-action=\"cancel\"]',\n }\n};\n"],"names":["ELEMENTS","AIDRAWER","AIDRAWER_BODY","PAGE","MAIN_REGION","AIDRAWER_CLOSE","RESPONSE","JUMPTO","ACTION","ACTIONS","SUMMARY","EXPLAIN","RETRY","DECLINE","ACCEPT","REGENERATE","CANCEL"],"mappings":"oLAsBe,CACXA,SAAU,CACNC,SAAU,aACVC,cAAe,6BACfC,KAAM,QACNC,YAAa,gBACbC,eAAgB,mBAChBC,SAAU,0BACVC,OAAQ,iDACRC,OAAQ,sDAEZC,QAAS,CACLC,QAAS,yDACTC,QAAS,uDACTC,MAAO,gDACPC,QAAS,2CACTC,OAAQ,0CACRC,WAAY,qDACZC,OAAQ"}
|
||||
@@ -80,7 +80,7 @@ const AICourseAssist = class {
|
||||
if (summariseAction) {
|
||||
e.preventDefault();
|
||||
this.openAIDrawer();
|
||||
this.lastAction = 'summarise';
|
||||
this.lastAction = 'summarise_text';
|
||||
this.actionElement.focus();
|
||||
const isPolicyAccepted = await this.isPolicyAccepted();
|
||||
if (!isPolicyAccepted) {
|
||||
@@ -95,7 +95,7 @@ const AICourseAssist = class {
|
||||
if (explainAction) {
|
||||
e.preventDefault();
|
||||
this.openAIDrawer();
|
||||
this.lastAction = 'explain';
|
||||
this.lastAction = 'explain_text';
|
||||
this.actionElement.focus();
|
||||
const isPolicyAccepted = await this.isPolicyAccepted();
|
||||
if (!isPolicyAccepted) {
|
||||
@@ -126,20 +126,26 @@ const AICourseAssist = class {
|
||||
}
|
||||
});
|
||||
|
||||
// Focus on the AI drawer's close button when the jump-to element is focused.
|
||||
this.jumpToElement.addEventListener('focus', () => {
|
||||
this.aiDrawerCloseElement.focus();
|
||||
});
|
||||
// Check if there is course assist control region in the page.
|
||||
if (this.jumpToElement) {
|
||||
// Focus on the AI drawer's close button when the jump-to element is focused.
|
||||
this.jumpToElement.addEventListener('focus', () => {
|
||||
this.aiDrawerCloseElement.focus();
|
||||
});
|
||||
}
|
||||
|
||||
// Focus on the action element when the AI drawer container receives focus.
|
||||
this.aiDrawerElement.addEventListener('focus', () => {
|
||||
this.actionElement.focus();
|
||||
});
|
||||
|
||||
// Remove active from the action element when it loses focus.
|
||||
this.actionElement.addEventListener('blur', () => {
|
||||
this.actionElement.classList.remove('active');
|
||||
});
|
||||
// Check if the action element exists.
|
||||
if (this.actionElement) {
|
||||
// Remove active from the action element when it loses focus.
|
||||
this.actionElement.addEventListener('blur', () => {
|
||||
this.actionElement.classList.remove('active');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -323,12 +329,12 @@ const AICourseAssist = class {
|
||||
let params = {};
|
||||
|
||||
switch (action) {
|
||||
case 'summarise':
|
||||
case 'summarise_text':
|
||||
params.method = 'aiplacement_courseassist_summarise_text';
|
||||
params.heading = await getString('aisummary', 'aiplacement_courseassist');
|
||||
break;
|
||||
|
||||
case 'explain':
|
||||
case 'explain_text':
|
||||
params.method = 'aiplacement_courseassist_explain_text';
|
||||
params.heading = await getString('aiexplain', 'aiplacement_courseassist');
|
||||
break;
|
||||
|
||||
@@ -32,8 +32,8 @@ export default {
|
||||
ACTION: '.course-assist-controls [data-input-type="action"]',
|
||||
},
|
||||
ACTIONS: {
|
||||
SUMMARY: '.course-assist-controls [data-action="summarise"]',
|
||||
EXPLAIN: '.course-assist-controls [data-action="explain"]',
|
||||
SUMMARY: '.course-assist-controls [data-action="summarise_text"]',
|
||||
EXPLAIN: '.course-assist-controls [data-action="explain_text"]',
|
||||
RETRY: '.course-assist-controls [data-action="retry"]',
|
||||
DECLINE: '.ai-policy-block [data-action="decline"]',
|
||||
ACCEPT: '.ai-policy-block [data-action="accept"]',
|
||||
|
||||
@@ -78,7 +78,9 @@ class explain_text extends external_api {
|
||||
|
||||
// Check the user has permission to use the AI service.
|
||||
self::validate_context($context);
|
||||
if (!utils::is_course_assist_available($context)) {
|
||||
|
||||
// Check if AI Placement course assist is available.
|
||||
if (!utils::is_course_assist_available()) {
|
||||
throw new \moodle_exception('nocourseassist', 'aiplacement_courseassist');
|
||||
}
|
||||
|
||||
|
||||
@@ -78,7 +78,9 @@ class summarise_text extends external_api {
|
||||
|
||||
// Check the user has permission to use the AI service.
|
||||
self::validate_context($context);
|
||||
if (!utils::is_course_assist_available($context)) {
|
||||
|
||||
// Check if AI Placement course assist is available.
|
||||
if (!utils::is_course_assist_available()) {
|
||||
throw new \moodle_exception('nocourseassist', 'aiplacement_courseassist');
|
||||
}
|
||||
|
||||
|
||||
@@ -102,6 +102,6 @@ class assist_ui {
|
||||
}
|
||||
|
||||
// Check if the user has permission to use the AI service.
|
||||
return utils::is_course_assist_available($PAGE->context);
|
||||
return utils::is_course_assist_available();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,22 +29,17 @@ use core_ai\manager;
|
||||
*/
|
||||
class utils {
|
||||
/**
|
||||
* Check if AI Placement course assist is available for the context.
|
||||
* Check if AI Placement course assist is available.
|
||||
*
|
||||
* @param \context $context The context.
|
||||
* @return bool True if AI Placement course assist is available, false otherwise.
|
||||
*/
|
||||
public static function is_course_assist_available(\context $context): bool {
|
||||
public static function is_course_assist_available(): bool {
|
||||
[$plugintype, $pluginname] = explode('_', \core_component::normalize_componentname('aiplacement_courseassist'), 2);
|
||||
$pluginmanager = \core_plugin_manager::resolve_plugininfo_class($plugintype);
|
||||
if (!$pluginmanager::is_plugin_enabled($pluginname)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (empty(self::get_actions_available($context))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -52,37 +47,34 @@ class utils {
|
||||
* Get all the actions available and return action data for template.
|
||||
*
|
||||
* @param \context $context The context.
|
||||
* @param bool $checkcontext If true, check the action is available in context.
|
||||
* @return array Return the actions available with data.
|
||||
*/
|
||||
public static function get_actions_available(\context $context): array {
|
||||
public static function get_actions_available(\context $context, bool $checkcontext = true): array {
|
||||
$actions = [];
|
||||
$actionclasses = [
|
||||
summarise_text::class,
|
||||
explain_text::class,
|
||||
];
|
||||
$manager = \core\di::get(manager::class);
|
||||
$providers = $manager->get_providers_for_actions($actionclasses, true);
|
||||
|
||||
// Summarise text.
|
||||
if (has_capability('aiplacement/courseassist:summarise_text', $context)
|
||||
&& $manager->is_action_available(summarise_text::class)
|
||||
&& $manager->is_action_enabled('aiplacement_courseassist', summarise_text::class)
|
||||
&& !empty($providers[summarise_text::class])
|
||||
&& (!$checkcontext || $manager->is_action_enabled_in_context($context, summarise_text::class))
|
||||
) {
|
||||
$actions[] = [
|
||||
'action' => 'summarise',
|
||||
'action' => 'summarise_text',
|
||||
'buttontext' => get_string('summarise', 'aiplacement_courseassist'),
|
||||
'title' => get_string('summarise_tooltips', 'aiplacement_courseassist'),
|
||||
];
|
||||
}
|
||||
|
||||
// Explain text.
|
||||
if (has_capability('aiplacement/courseassist:explain_text', $context)
|
||||
&& $manager->is_action_available(explain_text::class)
|
||||
&& $manager->is_action_enabled('aiplacement_courseassist', explain_text::class)
|
||||
&& !empty($providers[explain_text::class])
|
||||
&& (!$checkcontext || $manager->is_action_enabled_in_context($context, explain_text::class))
|
||||
) {
|
||||
$actions[] = [
|
||||
'action' => 'explain',
|
||||
'action' => 'explain_text',
|
||||
'buttontext' => get_string('explain', 'aiplacement_courseassist'),
|
||||
'title' => get_string('explain_tooltips', 'aiplacement_courseassist'),
|
||||
];
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
Example context (json):
|
||||
{
|
||||
"isdropdown": true,
|
||||
"action": "explain",
|
||||
"action": "explain_text",
|
||||
"buttontext": "Explain",
|
||||
"title": "Create an AI-generated explanation of the page content"
|
||||
}
|
||||
|
||||
@@ -28,12 +28,12 @@
|
||||
"isdropdown": true,
|
||||
"actions": [
|
||||
{
|
||||
"action": "summarise",
|
||||
"action": "summarise_text",
|
||||
"buttontext": "Summarise",
|
||||
"title": "Create an AI-generated summary of the page content"
|
||||
},
|
||||
{
|
||||
"action": "explain",
|
||||
"action": "explain_text",
|
||||
"buttontext": "Explain",
|
||||
"title": "Create an AI-generated explanation of the page content"
|
||||
}
|
||||
|
||||
@@ -26,12 +26,12 @@
|
||||
{
|
||||
"actions": [
|
||||
{
|
||||
"action": "summarise",
|
||||
"action": "summarise_text",
|
||||
"buttontext": "Summarise",
|
||||
"title": "Create an AI-generated summary of the page content"
|
||||
},
|
||||
{
|
||||
"action": "explain",
|
||||
"action": "explain_text",
|
||||
"buttontext": "Explain",
|
||||
"title": "Create an AI-generated explanation of the page content"
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
{
|
||||
"content": "<p>Content to display</p>",
|
||||
"heading": "AI Explain",
|
||||
"action": "explain"
|
||||
"action": "explain_text"
|
||||
}
|
||||
}}
|
||||
<section class="course-assist-response card mb-3" data-action-performed="{{action}}">
|
||||
|
||||
@@ -16,9 +16,10 @@
|
||||
|
||||
namespace aiplacement_courseassist;
|
||||
|
||||
use core_ai\aiactions\generate_text;
|
||||
use core_ai\aiactions\summarise_text;
|
||||
use core_ai\manager;
|
||||
use core_ai\ai_test_trait;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
require_once(__DIR__ . '/../../../tests/ai_test_trait.php');
|
||||
|
||||
/**
|
||||
* AI Placement course assist utils test.
|
||||
@@ -29,103 +30,103 @@ use core_ai\manager;
|
||||
* @covers \aiplacement_courseassist\utils
|
||||
*/
|
||||
final class utils_test extends \advanced_testcase {
|
||||
use ai_test_trait;
|
||||
|
||||
/** @var array List of users. */
|
||||
private array $users;
|
||||
/** @var \stdClass Course object. */
|
||||
private \stdClass $course;
|
||||
/** @var \context_course Course context. */
|
||||
private \context_course $context;
|
||||
/** @var \stdClass Teacher role. */
|
||||
private \stdClass $teacherrole;
|
||||
|
||||
public function setUp(): void {
|
||||
global $DB;
|
||||
parent::setUp();
|
||||
|
||||
$this->resetAfterTest();
|
||||
$this->users[1] = $this->getDataGenerator()->create_user();
|
||||
$this->users[2] = $this->getDataGenerator()->create_user();
|
||||
$this->course = $this->getDataGenerator()->create_course();
|
||||
$this->context = \context_course::instance($this->course->id);
|
||||
$this->teacherrole = $DB->get_record('role', ['shortname' => 'editingteacher']);
|
||||
$this->getDataGenerator()->enrol_user($this->users[1]->id, $this->course->id, 'manager');
|
||||
$this->getDataGenerator()->enrol_user($this->users[2]->id, $this->course->id, 'editingteacher');
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for supported placement tests.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function course_assist_actions_available_provider(): array {
|
||||
return [
|
||||
'Two actions' => [
|
||||
'actionstouse' => [
|
||||
'summarise_text',
|
||||
'explain_text',
|
||||
],
|
||||
'expectedcount' => 2,
|
||||
],
|
||||
'Summarise only' => [
|
||||
'actionstouse' => [
|
||||
'summarise_text',
|
||||
],
|
||||
'expectedcount' => 1,
|
||||
],
|
||||
'Explain only' => [
|
||||
'actionstouse' => [
|
||||
'explain_text',
|
||||
],
|
||||
'expectedcount' => 1,
|
||||
],
|
||||
'No actions' => [
|
||||
'actionstouse' => [],
|
||||
'expectedcount' => 0,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test is_course_assist_available method.
|
||||
*/
|
||||
public function test_is_course_assist_available(): void {
|
||||
global $DB;
|
||||
$this->resetAfterTest();
|
||||
$user1 = $this->getDataGenerator()->create_user();
|
||||
$user2 = $this->getDataGenerator()->create_user();
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$context = \context_course::instance($course->id);
|
||||
$teacherrole = $DB->get_record('role', ['shortname' => 'editingteacher']);
|
||||
$this->getDataGenerator()->enrol_user($user1->id, $course->id, 'manager');
|
||||
$this->getDataGenerator()->enrol_user($user2->id, $course->id, 'editingteacher');
|
||||
|
||||
// Provider is not enabled.
|
||||
$this->setUser($user1);
|
||||
$this->assertFalse(utils::is_course_assist_available($context));
|
||||
|
||||
// Provider is enabled, but plugin is not enabled.
|
||||
set_config('enabled', 1, 'aiprovider_openai');
|
||||
set_config('apikey', '123', 'aiprovider_openai');
|
||||
set_config('enabled', 0, 'aiplacement_courseassist');
|
||||
$this->assertFalse(utils::is_course_assist_available($context));
|
||||
|
||||
// Plugin is enabled but user does not have capability.
|
||||
assign_capability('aiplacement/courseassist:summarise_text', CAP_PROHIBIT, $teacherrole->id, $context);
|
||||
assign_capability('aiplacement/courseassist:explain_text', CAP_PROHIBIT, $teacherrole->id, $context);
|
||||
$this->setUser($user2);
|
||||
set_config('enabled', 1, 'aiplacement_courseassist');
|
||||
$this->assertFalse(utils::is_course_assist_available($context));
|
||||
$this->assertTrue(utils::is_course_assist_available());
|
||||
|
||||
// Plugin is enabled, user has capability and placement action is not available.
|
||||
$this->setUser($user1);
|
||||
set_config('summarise_text', 0, 'aiplacement_courseassist');
|
||||
set_config('explain_text', 0, 'aiplacement_courseassist');
|
||||
$this->assertFalse(utils::is_course_assist_available($context));
|
||||
|
||||
// Plugin is enabled, user has capability and provider action is not available.
|
||||
$this->setUser($user1);
|
||||
set_config('summarise_text', 0, 'aiprovider_openai');
|
||||
set_config('summarise_text', 1, 'aiplacement_courseassist');
|
||||
set_config('explain_text', 0, 'aiprovider_openai');
|
||||
set_config('explain_text', 1, 'aiplacement_courseassist');
|
||||
$this->assertFalse(utils::is_course_assist_available($context));
|
||||
|
||||
// Plugin is enabled, user has capability, placement action is available and provider action is available.
|
||||
$mockmanager = $this->createMock(manager::class);
|
||||
$mockmanager->method('is_action_available')->willReturn(true);
|
||||
$mockmanager->method('is_action_enabled')->willReturn(true);
|
||||
$mockmanager->method('get_providers_for_actions')->willReturn([
|
||||
summarise_text::class => ['aiprovider_openai'],
|
||||
]);
|
||||
|
||||
\core\di::set(manager::class, function() use ($mockmanager) {
|
||||
return $mockmanager;
|
||||
});
|
||||
|
||||
$this->setUser($user1);
|
||||
set_config('summarise_text', 1, 'aiplacement_courseassist');
|
||||
set_config('explain_text', 1, 'aiprovider_openai');
|
||||
set_config('explain_text', 1, 'aiplacement_courseassist');
|
||||
$this->assertTrue(utils::is_course_assist_available($context));
|
||||
set_config('enabled', 0, 'aiplacement_courseassist');
|
||||
$this->assertFalse(utils::is_course_assist_available());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test get_actions_available method.
|
||||
*
|
||||
* @param array $actionstouse The actions to use.
|
||||
* @param int $expectedcount Expected count of actions.
|
||||
* @dataProvider course_assist_actions_available_provider
|
||||
*/
|
||||
public function test_get_actions_available(): void {
|
||||
global $DB;
|
||||
$this->resetAfterTest();
|
||||
$user1 = $this->getDataGenerator()->create_user();
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$context = \context_course::instance($course->id);
|
||||
$this->getDataGenerator()->enrol_user($user1->id, $course->id, 'manager');
|
||||
$this->setUser($user1);
|
||||
public function test_get_actions_available(
|
||||
array $actionstouse,
|
||||
int $expectedcount,
|
||||
): void {
|
||||
$this->setUser($this->users[2]);
|
||||
// Set up the provider with the required action config.
|
||||
$this->create_ai_provider($actionstouse, \aiprovider_openai\provider::class);
|
||||
set_config('enabled', 1, 'aiplacement_courseassist');
|
||||
|
||||
// Two actions enabled.
|
||||
set_config('enabled', 1, 'aiprovider_openai');
|
||||
set_config('apikey', '123', 'aiprovider_openai');
|
||||
set_config('explain_text', 1, 'aiplacement_courseassist');
|
||||
set_config('summarise_text', 1, 'aiplacement_courseassist');
|
||||
$manager = \core\di::get(manager::class);
|
||||
$manager->create_provider_instance(
|
||||
classname: '\aiprovider_openai\provider',
|
||||
name: 'dummy',
|
||||
enabled: true,
|
||||
config: ['apikey' => '123'],
|
||||
);
|
||||
$this->assertCount(2, utils::get_actions_available($context));
|
||||
// Enable the actions and check the count.
|
||||
foreach ($actionstouse as $action) {
|
||||
set_config($action, 1, 'aiplacement_courseassist');
|
||||
}
|
||||
$actions = utils::get_actions_available($this->context, true);
|
||||
$this->assertCount($expectedcount, $actions);
|
||||
|
||||
// One action enabled.
|
||||
set_config('summarise_text', 0, 'aiplacement_courseassist');
|
||||
$this->assertCount(1, utils::get_actions_available($context));
|
||||
|
||||
// No actions enabled.
|
||||
set_config('explain_text', 0, 'aiplacement_courseassist');
|
||||
$this->assertCount(0, utils::get_actions_available($context));
|
||||
// Prohibit the user and check again.
|
||||
foreach ($actionstouse as $action) {
|
||||
assign_capability("aiplacement/courseassist:{$action}", CAP_PROHIBIT, $this->teacherrole->id, $this->context);
|
||||
}
|
||||
$actions = utils::get_actions_available($this->context, true);
|
||||
$this->assertCount(0, $actions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
namespace aiplacement_editor;
|
||||
|
||||
use core_ai\aiactions\generate_image;
|
||||
use core_ai\aiactions\generate_text;
|
||||
use core_ai\manager;
|
||||
|
||||
/**
|
||||
@@ -33,16 +35,16 @@ class utils {
|
||||
* @param \context $context The context.
|
||||
* @param string $actionname The name of the action.
|
||||
* @param string $actionclass The class name of the action.
|
||||
* @return bool True if the action is available, false otherwise.
|
||||
* @param bool $checkcontext If true, check the action is available in context.
|
||||
* @return bool If the action is accessible, available, and enable.
|
||||
*/
|
||||
public static function is_html_editor_placement_action_available(
|
||||
\context $context,
|
||||
string $actionname,
|
||||
string $actionclass
|
||||
string $actionclass,
|
||||
bool $checkcontext = true,
|
||||
): bool {
|
||||
[$plugintype, $pluginname] = explode('_', \core_component::normalize_componentname('aiplacement_editor'), 2);
|
||||
$pluginmanager = \core_plugin_manager::resolve_plugininfo_class($plugintype);
|
||||
if (!$pluginmanager::is_plugin_enabled($pluginname)) {
|
||||
if (!self::is_html_editor_placement_available()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -51,9 +53,57 @@ class utils {
|
||||
has_capability("aiplacement/editor:{$actionname}", $context)
|
||||
&& $aimanager->is_action_available($actionclass)
|
||||
&& $aimanager->is_action_enabled('aiplacement_editor', $actionclass)
|
||||
&& (!$checkcontext || $aimanager->is_action_enabled_in_context($context, $actionclass))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if AI Placement HTML editor is available.
|
||||
*
|
||||
* @return bool If the placement is enabled.
|
||||
*/
|
||||
public static function is_html_editor_placement_available(): bool {
|
||||
[$plugintype, $pluginname] = explode('_', \core_component::normalize_componentname('aiplacement_editor'), 2);
|
||||
$pluginmanager = \core_plugin_manager::resolve_plugininfo_class($plugintype);
|
||||
if (!$pluginmanager::is_plugin_enabled($pluginname)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the actions available for HTML editor placement.
|
||||
*
|
||||
* @param \context $context The context.
|
||||
* @param bool $checkcontext If true, check the action is available in context.
|
||||
* @return array Return the actions available with data.
|
||||
*/
|
||||
public static function get_actions_available(\context $context, bool $checkcontext = true): array {
|
||||
$actions = [];
|
||||
|
||||
// Action generate_text.
|
||||
if (self::is_html_editor_placement_action_available($context, 'generate_text', generate_text::class, $checkcontext)) {
|
||||
$actions[] = [
|
||||
'action' => 'generate_text',
|
||||
'buttontext' => get_string('action_generate_text', 'core_ai'),
|
||||
'title' => get_string('action_generate_text_desc', 'core_ai'),
|
||||
];
|
||||
}
|
||||
|
||||
// Action generate_image.
|
||||
if (self::is_html_editor_placement_action_available($context, 'generate_image', generate_image::class, $checkcontext)) {
|
||||
$actions[] = [
|
||||
'action' => 'generate_image',
|
||||
'buttontext' => get_string('action_generate_image', 'core_ai'),
|
||||
'title' => get_string('action_generate_image_desc', 'core_ai'),
|
||||
];
|
||||
}
|
||||
|
||||
return $actions;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ final class generate_text_test extends \advanced_testcase {
|
||||
$mockmanager->method('process_action')->willReturn($response);
|
||||
$mockmanager->method('is_action_available')->willReturn(true);
|
||||
$mockmanager->method('is_action_enabled')->willReturn(true);
|
||||
$mockmanager->method('is_action_enabled_in_context')->willReturn(true);
|
||||
\core\di::set(\core_ai\manager::class, function() use ($mockmanager) {
|
||||
return $mockmanager;
|
||||
});
|
||||
|
||||
@@ -16,8 +16,10 @@
|
||||
|
||||
namespace aiplacement_editor;
|
||||
|
||||
use core_ai\aiactions\generate_image;
|
||||
use core_ai\aiactions\generate_text;
|
||||
use core_ai\ai_test_trait;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
require_once(__DIR__ . '/../../../tests/ai_test_trait.php');
|
||||
|
||||
/**
|
||||
* Text editor placement utils test.
|
||||
@@ -25,9 +27,11 @@ use core_ai\aiactions\generate_text;
|
||||
* @package aiplacement_editor
|
||||
* @copyright 2024 Huong Nguyen <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \aiplacement_courseassist\utils
|
||||
* @covers \aiplacement_editor\utils
|
||||
*/
|
||||
final class utils_test extends \advanced_testcase {
|
||||
use ai_test_trait;
|
||||
|
||||
/** @var array List of users. */
|
||||
private array $users;
|
||||
/** @var \stdClass Course object. */
|
||||
@@ -52,36 +56,52 @@ final class utils_test extends \advanced_testcase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Test is_html_editor_placement_action_available method.
|
||||
* Data provider for supported placement tests.
|
||||
*
|
||||
* @param string $actionname Action name.
|
||||
* @param string $actionclass Action class.
|
||||
* @dataProvider html_editor_placement_action_available_provider
|
||||
* @return array
|
||||
*/
|
||||
public function test_is_html_editor_placement_action_available(
|
||||
string $actionname,
|
||||
string $actionclass,
|
||||
): void {
|
||||
// Provider is not enabled.
|
||||
$this->setUser($this->users[1]);
|
||||
$this->assertFalse(utils::is_html_editor_placement_action_available(
|
||||
context: $this->context,
|
||||
actionname: $actionname,
|
||||
actionclass: $actionclass
|
||||
));
|
||||
public static function html_editor_placement_actions_available_provider(): array {
|
||||
return [
|
||||
'Two actions' => [
|
||||
'actionstouse' => [
|
||||
'generate_text',
|
||||
'generate_image',
|
||||
],
|
||||
'expectedcount' => 2,
|
||||
],
|
||||
'Generate text only' => [
|
||||
'actionstouse' => [
|
||||
'generate_text',
|
||||
],
|
||||
'expectedcount' => 1,
|
||||
],
|
||||
'Generate image only' => [
|
||||
'actionstouse' => [
|
||||
'generate_image',
|
||||
],
|
||||
'expectedcount' => 1,
|
||||
],
|
||||
'No actions' => [
|
||||
'actionstouse' => [],
|
||||
'expectedcount' => 0,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
// Plugin is not enabled.
|
||||
$this->setUser($this->users[1]);
|
||||
/**
|
||||
* Test is_html_editor_placement_action_available method.
|
||||
*/
|
||||
public function test_is_html_editor_placement_action_available(): void {
|
||||
// Everything is disabled to begin with, and user does not have capability.
|
||||
// Sequentially enable settings until all conditions are met.
|
||||
$actionname = 'generate_text';
|
||||
$actionclass = 'core_ai\\aiactions\\' . $actionname;
|
||||
set_config('enabled', 0, 'aiplacement_editor');
|
||||
$this->assertFalse(utils::is_html_editor_placement_action_available(
|
||||
context: $this->context,
|
||||
actionname: $actionname,
|
||||
actionclass: $actionclass
|
||||
));
|
||||
|
||||
// Plugin is enabled but user does not have capability.
|
||||
set_config($actionname, 0, 'aiplacement_editor');
|
||||
assign_capability("aiplacement/editor:{$actionname}", CAP_PROHIBIT, $this->teacherrole->id, $this->context);
|
||||
$this->setUser($this->users[2]);
|
||||
|
||||
// Enable the placement plugin.
|
||||
set_config('enabled', 1, 'aiplacement_editor');
|
||||
$this->assertFalse(utils::is_html_editor_placement_action_available(
|
||||
context: $this->context,
|
||||
@@ -89,33 +109,25 @@ final class utils_test extends \advanced_testcase {
|
||||
actionclass: $actionclass
|
||||
));
|
||||
|
||||
// Plugin is enabled, user has capability and placement action is not available.
|
||||
$this->setUser($this->users[1]);
|
||||
set_config($actionname, 0, 'aiplacement_editor');
|
||||
// Enable the provider.
|
||||
$this->create_ai_provider([$actionname], \aiprovider_openai\provider::class);
|
||||
$this->assertFalse(utils::is_html_editor_placement_action_available(
|
||||
context: $this->context,
|
||||
actionname: $actionname,
|
||||
actionclass: $actionclass
|
||||
));
|
||||
|
||||
// Plugin is enabled, user has capability and provider action is not available.
|
||||
// Switch to a user with the required capability.
|
||||
$this->setUser($this->users[1]);
|
||||
$this->assertFalse(utils::is_html_editor_placement_action_available(
|
||||
context: $this->context,
|
||||
actionname: $actionname,
|
||||
actionclass: $actionclass
|
||||
));
|
||||
|
||||
// Enable the action for the placement plugin.
|
||||
// All requirements should now be met.
|
||||
set_config($actionname, 1, 'aiplacement_editor');
|
||||
$this->assertFalse(utils::is_html_editor_placement_action_available(
|
||||
context: $this->context,
|
||||
actionname: $actionname,
|
||||
actionclass: $actionclass
|
||||
));
|
||||
|
||||
// Plugin is enabled, user has capability, placement action is available and provider action is available.
|
||||
$mockmanager = $this->createMock(\core_ai\manager::class);
|
||||
$mockmanager->method('is_action_available')->willReturn(true);
|
||||
$mockmanager->method('is_action_enabled')->willReturn(true);
|
||||
|
||||
\core\di::set(\core_ai\manager::class, function() use ($mockmanager) {
|
||||
return $mockmanager;
|
||||
});
|
||||
$this->setUser($this->users[1]);
|
||||
$this->assertTrue(utils::is_html_editor_placement_action_available(
|
||||
context: $this->context,
|
||||
actionname: $actionname,
|
||||
@@ -124,20 +136,46 @@ final class utils_test extends \advanced_testcase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for {@see test_is_html_editor_placement_action_available}
|
||||
* Test get_actions_available method.
|
||||
*
|
||||
* @return array
|
||||
* @param array $actionstouse The actions to use.
|
||||
* @param int $expectedcount Expected count of actions.
|
||||
* @dataProvider html_editor_placement_actions_available_provider
|
||||
*/
|
||||
public static function html_editor_placement_action_available_provider(): array {
|
||||
return [
|
||||
'Text generation' => [
|
||||
'generate_text',
|
||||
generate_text::class,
|
||||
],
|
||||
'Image generation' => [
|
||||
'generate_image',
|
||||
generate_image::class,
|
||||
],
|
||||
];
|
||||
public function test_get_actions_available(
|
||||
array $actionstouse,
|
||||
int $expectedcount,
|
||||
): void {
|
||||
$this->setUser($this->users[2]);
|
||||
// Set up the provider with the required action config.
|
||||
$this->create_ai_provider($actionstouse, \aiprovider_openai\provider::class);
|
||||
set_config('enabled', 1, 'aiplacement_editor');
|
||||
|
||||
// Enable the actions and check the count.
|
||||
foreach ($actionstouse as $action) {
|
||||
set_config($action, 1, 'aiplacement_editor');
|
||||
}
|
||||
$actions = utils::get_actions_available($this->context, true);
|
||||
$this->assertCount($expectedcount, $actions);
|
||||
|
||||
// Prohibit the user and check again.
|
||||
foreach ($actionstouse as $action) {
|
||||
assign_capability("aiplacement/editor:{$action}", CAP_PROHIBIT, $this->teacherrole->id, $this->context);
|
||||
}
|
||||
$actions = utils::get_actions_available($this->context, true);
|
||||
$this->assertCount(0, $actions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test is_html_editor_placement_available method.
|
||||
*/
|
||||
public function test_is_html_editor_placement_available(): void {
|
||||
// Plugin is not enabled.
|
||||
set_config('enabled', 0, 'aiplacement_editor');
|
||||
$this->assertFalse(utils::is_html_editor_placement_available());
|
||||
|
||||
// Plugin is enabled.
|
||||
set_config('enabled', 1, 'aiplacement_editor');
|
||||
$this->assertTrue(utils::is_html_editor_placement_available());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<?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_ai;
|
||||
|
||||
/**
|
||||
* Test trait for AI.
|
||||
*
|
||||
* @package core_ai
|
||||
* @category test
|
||||
* @copyright 2025 Stevani Andolo <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
trait ai_test_trait {
|
||||
/**
|
||||
* Creates a dummy AI provider.
|
||||
*
|
||||
* @param array $actions A set of actions to configure the provider with.
|
||||
* @param string $providerclass
|
||||
*/
|
||||
private function create_ai_provider(array $actions, $providerclass): void {
|
||||
global $DB;
|
||||
|
||||
$actionconfig = [];
|
||||
foreach ($actions as $action) {
|
||||
$actionclass = 'core_ai\\aiactions\\' . $action;
|
||||
$actionconfig[$actionclass] = [
|
||||
'enabled' => true,
|
||||
'settings' => [
|
||||
'model' => 'test',
|
||||
'endpoint' => 'test',
|
||||
'systeminstruction' => 'test',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$config = ['apikey' => 'test'];
|
||||
$record = new \stdClass();
|
||||
$record->name = 'test';
|
||||
$record->provider = $providerclass;
|
||||
$record->enabled = 1;
|
||||
$record->config = json_encode($config);
|
||||
$record->actionconfig = json_encode($actionconfig);
|
||||
$DB->insert_record('ai_providers', $record);
|
||||
}
|
||||
}
|
||||
@@ -746,4 +746,142 @@ final class manager_test extends \advanced_testcase {
|
||||
$result = $manager->is_action_available($action);
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test is_ai_tools_enabled_in_course method.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_is_ai_tools_enabled_in_course(): void {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
$generator = $this->getDataGenerator();
|
||||
$course = $generator->create_course();
|
||||
$context = \context_course::instance($course->id);
|
||||
|
||||
$manager = \core\di::get(manager::class);
|
||||
$aitoolsenabled = $manager::is_ai_tools_enabled_in_course($context);
|
||||
$this->assertTrue($aitoolsenabled);
|
||||
|
||||
$course->enableaitools = 0;
|
||||
$DB->update_record('course', $course);
|
||||
|
||||
$aitoolsenabled = $manager::is_ai_tools_enabled_in_course($context);
|
||||
$this->assertFalse($aitoolsenabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test get_enabled_actions_in_course_module method.
|
||||
*
|
||||
* @param string $enabledactions
|
||||
* @param array $expectedactions
|
||||
* @dataProvider ai_actions_provider
|
||||
* @return void
|
||||
*/
|
||||
public function test_get_enabled_actions_in_course_module(
|
||||
string $enabledactions,
|
||||
array $expectedactions,
|
||||
): void {
|
||||
global $PAGE;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
$manager = \core\di::get(manager::class);
|
||||
$generator = $this->getDataGenerator();
|
||||
$course = $generator->create_course();
|
||||
|
||||
// Create forum module and add enabled AI actions.
|
||||
$module = $generator->create_module('forum', [
|
||||
'course' => $course->id,
|
||||
'enabledaiactions' => $enabledactions,
|
||||
]);
|
||||
|
||||
// Set the page context to the module context.
|
||||
$ctx = \context_module::instance($module->cmid);
|
||||
$PAGE->set_context($ctx);
|
||||
|
||||
// Get all enabled actions in a course module.
|
||||
$record = $manager::get_ai_fields_from_course_module($module->cmid);
|
||||
$allactions = $manager::get_enabled_actions_in_course_module($record);
|
||||
foreach ($expectedactions as $expectedaction) {
|
||||
$this->assertContains($expectedaction, $allactions);
|
||||
}
|
||||
$this->assertCount(count($expectedactions), $allactions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test is_action_enabled_in_context method.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_is_action_enabled_in_context(): void {
|
||||
global $PAGE;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
$manager = \core\di::get(manager::class);
|
||||
$generator = $this->getDataGenerator();
|
||||
$course = $generator->create_course();
|
||||
|
||||
// Create forum module and enabled only the generate text action.
|
||||
$module = $generator->create_module('forum', [
|
||||
'course' => $course->id,
|
||||
'enabledaiactions' => json_encode(['generate_text' => 1]),
|
||||
]);
|
||||
|
||||
// Set the page context to the module context.
|
||||
$modulecontext = \context_module::instance($module->cmid);
|
||||
$PAGE->set_context($modulecontext);
|
||||
|
||||
// Only the generate text action should be available.
|
||||
$result = $manager->is_action_enabled_in_context($modulecontext, generate_text::class);
|
||||
$this->assertTrue($result);
|
||||
$result = $manager->is_action_enabled_in_context($modulecontext, explain_text::class);
|
||||
$this->assertFalse($result);
|
||||
|
||||
// Explain text should be available outside the module context.
|
||||
$systemcontext = \context_system::instance();
|
||||
$result = $manager->is_action_enabled_in_context($systemcontext, explain_text::class);
|
||||
$this->assertTrue($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for {@see test_get_enabled_actions_in_course_module}
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function ai_actions_provider(): array {
|
||||
return [
|
||||
'actioncombo1' => [
|
||||
json_encode(['generate_text' => 1, 'generate_image' => 1]),
|
||||
[
|
||||
generate_text::class,
|
||||
generate_image::class,
|
||||
],
|
||||
],
|
||||
'actioncombo2' => [
|
||||
json_encode(['summarise_text' => 1, 'explain_text' => 1]),
|
||||
[
|
||||
summarise_text::class,
|
||||
explain_text::class,
|
||||
],
|
||||
],
|
||||
'actioncombo3' => [
|
||||
json_encode(['summarise_text' => 1, 'explain_text' => 0, 'generate_text' => 1, 'generate_image' => 1]),
|
||||
[
|
||||
summarise_text::class,
|
||||
generate_text::class,
|
||||
generate_image::class,
|
||||
],
|
||||
],
|
||||
'actioncombo4' => [
|
||||
json_encode(['summarise_text' => 0, 'explain_text' => 1, 'generate_text' => 0, 'generate_image' => 0]),
|
||||
[
|
||||
explain_text::class,
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ $string['acceptai'] = 'Accept and continue';
|
||||
$string['action'] = 'Action';
|
||||
$string['action_explain_text'] = 'Explain text';
|
||||
$string['action_explain_text_desc'] = 'Explains the text content on a course page.';
|
||||
$string['action_explain_text_help'] = 'Provides an explanation that expands on key ideas, simplifies complex concepts, and adds context to make the text easier to understand.';
|
||||
$string['action_explain_text_instruction'] = 'You will receive a text input from the user. Your task is to explain the provided text. Follow these guidelines:
|
||||
1. Elaborate: Expand on key ideas and concepts, ensuring the explanation adds meaningful depth and avoids restating the text verbatim.
|
||||
2. Simplify: Break down complex terms or ideas into simpler components, making them easy to understand for a wide audience, including learners.
|
||||
@@ -40,13 +41,16 @@ Important Instructions:
|
||||
Ensure the explanation is easy to read and effectively conveys the main points of the original text.';
|
||||
$string['action_generate_image'] = 'Generate image';
|
||||
$string['action_generate_image_desc'] = 'Generates an image based on a text prompt.';
|
||||
$string['action_generate_image_help'] = 'Creates an image based on a prompt.';
|
||||
$string['action_generate_text'] = 'Generate text';
|
||||
$string['action_generate_text_desc'] = 'Generates text based on a text prompt.';
|
||||
$string['action_generate_text_help'] = 'Creates a text based on a prompt.';
|
||||
$string['action_generate_text_instruction'] = 'You will receive a text input from the user. Your task is to generate text based on their request. Follow these important instructions:
|
||||
1. Return the summary in plain text only.
|
||||
2. Do not include any markdown formatting, greetings, or platitudes.';
|
||||
$string['action_summarise_text'] = 'Summarise text';
|
||||
$string['action_summarise_text_desc'] = 'Summarises the text content on a course page.';
|
||||
$string['action_summarise_text_help'] = 'Creates a brief summary of the content in a page.';
|
||||
$string['action_summarise_text_instruction'] = 'You will receive a text input from the user. Your task is to summarize the provided text. Follow these guidelines:
|
||||
1. Condense: Shorten long passages into key points.
|
||||
2. Simplify: Make complex information easier to understand, especially for learners.
|
||||
@@ -64,12 +68,17 @@ $string['actionsettingprovider_desc'] = 'These settings control how the {$a->pro
|
||||
$string['actionsettings'] = 'Action settings';
|
||||
$string['actionsettings_desc'] = 'These settings control the AI actions for this provider instance.';
|
||||
$string['ai'] = 'AI';
|
||||
$string['aiactionshdr'] = 'Select AI features for this activity:';
|
||||
$string['aiactionregister'] = 'AI action register';
|
||||
$string['aiplacements'] = 'AI placements';
|
||||
$string['aipolicyacceptance'] = 'AI policy acceptance';
|
||||
$string['aipolicyregister'] = 'AI policy register';
|
||||
$string['aiproviders'] = 'AI providers';
|
||||
$string['aireports'] = 'AI reports';
|
||||
$string['aitools'] = 'AI tools';
|
||||
$string['aitoolsincourseactivitydesc'] = 'If set to Yes, you can specify which AI features will be available.';
|
||||
$string['aitoolsincoursedesc'] = 'If set to Yes, AI tools will be available for activities in this course. AI tools can be configured in each activity\'s setting.';
|
||||
$string['aitoolsnotenabled'] = 'To specify which AI features you want to be available in this activity. Go to course settings and allow AI Tools.';
|
||||
$string['aiusage'] = 'AI usage';
|
||||
$string['aiusagepolicy'] = 'AI usage policy';
|
||||
$string['availableplacements'] = 'Choose where AI actions are available';
|
||||
@@ -86,6 +95,8 @@ $string['contentwatermark'] = 'Generated by AI';
|
||||
$string['createnewprovider'] = 'Create a new provider instance';
|
||||
$string['dateaccepted'] = 'Date accepted';
|
||||
$string['declineaipolicy'] = 'Decline';
|
||||
$string['enableaitoolsincourse'] = 'Allow AI tools for this course';
|
||||
$string['enableaitoolsincourseactivity'] = 'Allow AI tools in this activity';
|
||||
$string['enableglobalratelimit'] = 'Set site-wide rate limit';
|
||||
$string['enableglobalratelimit_help'] = 'Limit the number of requests that the AI provider can receive across the entire site every hour.';
|
||||
$string['enableuserratelimit'] = 'Set user rate limit';
|
||||
@@ -113,6 +124,8 @@ $string['globalratelimit_help'] = 'The number of site-wide requests allowed per
|
||||
$string['manageaiplacements'] = 'Manage AI placements';
|
||||
$string['manageaiproviders'] = 'Manage AI providers';
|
||||
$string['noproviders'] = 'This action is unavailable. No <a href="{$a}">AI providers</a> are configured for this action.';
|
||||
$string['off'] = 'Off';
|
||||
$string['on'] = 'On';
|
||||
$string['placement'] = 'Placement';
|
||||
$string['placementactionsettings'] = 'Actions';
|
||||
$string['placementactionsettings_desc'] = 'The AI actions available for this placement.';
|
||||
|
||||
@@ -1394,7 +1394,7 @@ function get_all_instances_in_courses($modulename, $courses, $userid=NULL, $incl
|
||||
$params['modulename'] = $modulename;
|
||||
|
||||
if (!$rawmods = $DB->get_records_sql("SELECT cm.id AS coursemodule, m.*, cw.section, cm.visible AS visible,
|
||||
cm.groupmode, cm.groupingid, cm.lang
|
||||
cm.groupmode, cm.groupingid, cm.lang, cm.enableaitools, cm.enabledaiactions
|
||||
FROM {course_modules} cm, {course_sections} cw, {modules} md,
|
||||
{".$modulename."} m
|
||||
WHERE cm.course $coursessql AND
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
<FIELD NAME="showactivitydates" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Whether to display activity dates to user. 0 = do not display, 1 = display activity dates"/>
|
||||
<FIELD NAME="showcompletionconditions" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Whether to display completion conditions to user. 0 = do not display, 1 = display conditions"/>
|
||||
<FIELD NAME="pdfexportfont" TYPE="char" LENGTH="50" NOTNULL="false" SEQUENCE="false"/>
|
||||
<FIELD NAME="enableaitools" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Whether to allow the use of AI tools in this course. 1 = enabled, 0 = disabled."/>
|
||||
</FIELDS>
|
||||
<KEYS>
|
||||
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
|
||||
@@ -345,6 +346,8 @@
|
||||
<FIELD NAME="deletioninprogress" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
<FIELD NAME="downloadcontent" TYPE="int" LENGTH="1" NOTNULL="false" DEFAULT="1" SEQUENCE="false" COMMENT="Whether the ability to download course module content is enabled for this activity"/>
|
||||
<FIELD NAME="lang" TYPE="char" LENGTH="30" NOTNULL="false" SEQUENCE="false" COMMENT="Forced language for this activity. Null or '' means 'Do not force'. Otherwise a Moodle lang pack name like 'fr' or 'en_us'."/>
|
||||
<FIELD NAME="enableaitools" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Whether to allow the use of AI tools in this course module. 1 = enabled, 0 = disabled."/>
|
||||
<FIELD NAME="enabledaiactions" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="List of enabled AI actions on this course module"/>
|
||||
</FIELDS>
|
||||
<KEYS>
|
||||
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
|
||||
|
||||
@@ -2094,5 +2094,36 @@ function xmldb_main_upgrade($oldversion) {
|
||||
upgrade_main_savepoint(true, 2025082900.01);
|
||||
}
|
||||
|
||||
if ($oldversion < 2025090200.01) {
|
||||
// Define field enableaitools to be added to course.
|
||||
$table = new xmldb_table('course');
|
||||
$field = new xmldb_field('enableaitools', XMLDB_TYPE_INTEGER, '1', null, null, null, null, 'pdfexportfont');
|
||||
|
||||
// Conditionally launch add field enableaitools.
|
||||
if (!$dbman->field_exists($table, $field)) {
|
||||
$dbman->add_field($table, $field);
|
||||
}
|
||||
|
||||
// Define field enableaitools to be added to course_modules.
|
||||
$table = new xmldb_table('course_modules');
|
||||
$field = new xmldb_field('enableaitools', XMLDB_TYPE_INTEGER, '1', null, null, null, null, 'lang');
|
||||
|
||||
// Conditionally launch add field enableaitools.
|
||||
if (!$dbman->field_exists($table, $field)) {
|
||||
$dbman->add_field($table, $field);
|
||||
}
|
||||
|
||||
// Define field enabledaiactions to be added to course_modules.
|
||||
$field = new xmldb_field('enabledaiactions', XMLDB_TYPE_TEXT, null, null, null, null, null, 'enableaitools');
|
||||
|
||||
// Conditionally launch add field enabledaiactions.
|
||||
if (!$dbman->field_exists($table, $field)) {
|
||||
$dbman->add_field($table, $field);
|
||||
}
|
||||
|
||||
// Main savepoint reached.
|
||||
upgrade_main_savepoint(true, 2025090200.01);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$version = 2025090200.00; // YYYYMMDD = weekly release date of this DEV branch.
|
||||
$version = 2025090200.01; // YYYYMMDD = weekly release date of this DEV branch.
|
||||
// RR = release increments - 00 in DEV branches.
|
||||
// .XX = incremental changes.
|
||||
$release = '5.1dev+ (Build: 20250902)'; // Human-friendly version name
|
||||
|
||||
Reference in New Issue
Block a user