diff --git a/mod/lti/amd/build/course_tools_list.min.js b/mod/lti/amd/build/course_tools_list.min.js new file mode 100644 index 0000000000000..991eba8c9d2a8 --- /dev/null +++ b/mod/lti/amd/build/course_tools_list.min.js @@ -0,0 +1,3 @@ +define("mod_lti/course_tools_list",["exports","core/notification","core/pending","core/ajax","core/toast","core/prefetch","core/str","core_table/dynamic","core_table/local/dynamic/selectors"],(function(_exports,_notification,_pending,_ajax,_toast,_prefetch,_str,_dynamic,Selectors){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending),_ajax=_interopRequireDefault(_ajax),Selectors=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Selectors);_exports.init=()=>{(0,_prefetch.prefetchStrings)("mod_lti",["deletecoursetool","deletecoursetoolconfirm","coursetooldeleted"]),(0,_prefetch.prefetchStrings)("core",["delete"]),document.addEventListener("click",(event=>{const courseToolDelete=event.target.closest('[data-action="course-tool-delete"]');if(courseToolDelete){event.preventDefault();const triggerElement=courseToolDelete.closest(".dropdown").querySelector(".dropdown-toggle");_notification.default.saveCancelPromise((0,_str.get_string)("deletecoursetool","mod_lti"),(0,_str.get_string)("deletecoursetoolconfirm","mod_lti",courseToolDelete.dataset.courseToolName),(0,_str.get_string)("delete","core"),{triggerElement:triggerElement}).then((()=>{const pendingPromise=new _pending.default("mod_lti/course_tools:delete"),request={methodname:"mod_lti_delete_course_tool_type",args:{tooltypeid:courseToolDelete.dataset.courseToolId}};return _ajax.default.call([request])[0].then((0,_toast.add)((0,_str.get_string)("coursetooldeleted","mod_lti"))).then((()=>{const tableRoot=triggerElement.closest(Selectors.main.region);return(0,_dynamic.refreshTableContent)(tableRoot)})).then(pendingPromise.resolve).catch(_notification.default.exception)})).catch((()=>{}))}}))}})); + +//# sourceMappingURL=course_tools_list.min.js.map \ No newline at end of file diff --git a/mod/lti/amd/build/course_tools_list.min.js.map b/mod/lti/amd/build/course_tools_list.min.js.map new file mode 100644 index 0000000000000..13e18320ae405 --- /dev/null +++ b/mod/lti/amd/build/course_tools_list.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"course_tools_list.min.js","sources":["../src/course_tools_list.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 .\n\n/**\n * Course LTI External tools list management.\n *\n * @module mod_lti/course_tools_list\n * @copyright 2023 Jake Dallimore \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\"use strict\";\n\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport Ajax from 'core/ajax';\nimport {add as addToast} from 'core/toast';\nimport {prefetchStrings} from 'core/prefetch';\nimport {get_string as getString} from 'core/str';\nimport {refreshTableContent} from 'core_table/dynamic';\nimport * as Selectors from 'core_table/local/dynamic/selectors';\n\n/**\n * Initialise module.\n */\nexport const init = () => {\n prefetchStrings('mod_lti', [\n 'deletecoursetool',\n 'deletecoursetoolconfirm',\n 'coursetooldeleted'\n ]);\n\n prefetchStrings('core', [\n 'delete',\n ]);\n\n document.addEventListener('click', event => {\n\n const courseToolDelete = event.target.closest('[data-action=\"course-tool-delete\"]');\n if (courseToolDelete) {\n event.preventDefault();\n\n // Use triggerElement to return focus to the action menu toggle.\n const triggerElement = courseToolDelete.closest('.dropdown').querySelector('.dropdown-toggle');\n Notification.saveCancelPromise(\n getString('deletecoursetool', 'mod_lti'),\n getString('deletecoursetoolconfirm', 'mod_lti', courseToolDelete.dataset.courseToolName),\n getString('delete', 'core'),\n {triggerElement}\n ).then(() => {\n const pendingPromise = new Pending('mod_lti/course_tools:delete');\n\n const request = {\n methodname: 'mod_lti_delete_course_tool_type',\n args: {tooltypeid: courseToolDelete.dataset.courseToolId}\n };\n return Ajax.call([request])[0]\n .then(addToast(getString('coursetooldeleted', 'mod_lti')))\n .then(() => {\n const tableRoot = triggerElement.closest(Selectors.main.region);\n return refreshTableContent(tableRoot);\n })\n .then(pendingPromise.resolve)\n .catch(Notification.exception);\n }).catch(() => {\n return;\n });\n }\n });\n};\n"],"names":["document","addEventListener","event","courseToolDelete","target","closest","preventDefault","triggerElement","querySelector","saveCancelPromise","dataset","courseToolName","then","pendingPromise","Pending","request","methodname","args","tooltypeid","courseToolId","Ajax","call","tableRoot","Selectors","main","region","resolve","catch","Notification","exception"],"mappings":"8/CAqCoB,mCACA,UAAW,CACvB,mBACA,0BACA,oDAGY,OAAQ,CACpB,WAGJA,SAASC,iBAAiB,SAASC,cAEzBC,iBAAmBD,MAAME,OAAOC,QAAQ,yCAC1CF,iBAAkB,CAClBD,MAAMI,uBAGAC,eAAiBJ,iBAAiBE,QAAQ,aAAaG,cAAc,0CAC9DC,mBACT,mBAAU,mBAAoB,YAC9B,mBAAU,0BAA2B,UAAWN,iBAAiBO,QAAQC,iBACzE,mBAAU,SAAU,QACpB,CAACJ,eAAAA,iBACHK,MAAK,WACGC,eAAiB,IAAIC,iBAAQ,+BAE7BC,QAAU,CACZC,WAAY,kCACZC,KAAM,CAACC,WAAYf,iBAAiBO,QAAQS,sBAEzCC,cAAKC,KAAK,CAACN,UAAU,GACvBH,MAAK,eAAS,mBAAU,oBAAqB,aAC7CA,MAAK,WACIU,UAAYf,eAAeF,QAAQkB,UAAUC,KAAKC,eACjD,gCAAoBH,cAE9BV,KAAKC,eAAea,SACpBC,MAAMC,sBAAaC,cACzBF,OAAM"} \ No newline at end of file diff --git a/mod/lti/amd/src/course_tools_list.js b/mod/lti/amd/src/course_tools_list.js new file mode 100644 index 0000000000000..ca234c11436da --- /dev/null +++ b/mod/lti/amd/src/course_tools_list.js @@ -0,0 +1,82 @@ +// 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 . + +/** + * Course LTI External tools list management. + * + * @module mod_lti/course_tools_list + * @copyright 2023 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +"use strict"; + +import Notification from 'core/notification'; +import Pending from 'core/pending'; +import Ajax from 'core/ajax'; +import {add as addToast} from 'core/toast'; +import {prefetchStrings} from 'core/prefetch'; +import {get_string as getString} from 'core/str'; +import {refreshTableContent} from 'core_table/dynamic'; +import * as Selectors from 'core_table/local/dynamic/selectors'; + +/** + * Initialise module. + */ +export const init = () => { + prefetchStrings('mod_lti', [ + 'deletecoursetool', + 'deletecoursetoolconfirm', + 'coursetooldeleted' + ]); + + prefetchStrings('core', [ + 'delete', + ]); + + document.addEventListener('click', event => { + + const courseToolDelete = event.target.closest('[data-action="course-tool-delete"]'); + if (courseToolDelete) { + event.preventDefault(); + + // Use triggerElement to return focus to the action menu toggle. + const triggerElement = courseToolDelete.closest('.dropdown').querySelector('.dropdown-toggle'); + Notification.saveCancelPromise( + getString('deletecoursetool', 'mod_lti'), + getString('deletecoursetoolconfirm', 'mod_lti', courseToolDelete.dataset.courseToolName), + getString('delete', 'core'), + {triggerElement} + ).then(() => { + const pendingPromise = new Pending('mod_lti/course_tools:delete'); + + const request = { + methodname: 'mod_lti_delete_course_tool_type', + args: {tooltypeid: courseToolDelete.dataset.courseToolId} + }; + return Ajax.call([request])[0] + .then(addToast(getString('coursetooldeleted', 'mod_lti'))) + .then(() => { + const tableRoot = triggerElement.closest(Selectors.main.region); + return refreshTableContent(tableRoot); + }) + .then(pendingPromise.resolve) + .catch(Notification.exception); + }).catch(() => { + return; + }); + } + }); +}; diff --git a/mod/lti/classes/external/delete_course_tool_type.php b/mod/lti/classes/external/delete_course_tool_type.php new file mode 100644 index 0000000000000..cd03f2c40585e --- /dev/null +++ b/mod/lti/classes/external/delete_course_tool_type.php @@ -0,0 +1,81 @@ +. + +namespace mod_lti\external; + +use core_external\external_api; +use core_external\external_function_parameters; +use core_external\external_value; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/mod/lti/locallib.php'); + +/** + * External function to delete a course tool type. + * + * @package mod_lti + * @copyright 2023 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class delete_course_tool_type extends external_api { + + /** + * Get parameter definition. + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters([ + 'tooltypeid' => new external_value(PARAM_INT, 'Tool type ID'), + ]); + } + + /** + * Delete a course tool type. + * + * @param int $tooltypeid the id of the course external tool type. + * @return bool true + * @throws \invalid_parameter_exception if the provided id refers to a site level tool which cannot be deleted. + */ + public static function execute(int $tooltypeid): bool { + + ['tooltypeid' => $tooltypeid] = self::validate_parameters(self::execute_parameters(), ['tooltypeid' => $tooltypeid]); + + global $DB; + $course = (int) $DB->get_field('lti_types', 'course', ['id' => $tooltypeid]); + if ($course == get_site()->id) { + throw new \invalid_parameter_exception('This is a site-level tool and cannot be deleted via this service'); + } + + $context = \context_course::instance($course); + self::validate_context($context); + require_capability('mod/lti:addcoursetool', $context); + + \lti_delete_type($tooltypeid); + return true; + } + + /** + * Get service returns definition. + * + * @return external_value + */ + public static function execute_returns(): external_value { + return new external_value(PARAM_BOOL, 'Success'); + } +} diff --git a/mod/lti/coursetools.php b/mod/lti/coursetools.php index dd5a5492a1993..e1192d0b63364 100644 --- a/mod/lti/coursetools.php +++ b/mod/lti/coursetools.php @@ -21,6 +21,7 @@ * @copyright 2023 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ + use mod_lti\output\course_tools_page; require_once("../../config.php"); @@ -39,7 +40,7 @@ } // Page setup. -global $PAGE; +global $PAGE, $OUTPUT; $pagetitle = get_string('courseexternaltools', 'mod_lti'); $pageurl = new moodle_url('/mod/lti/coursetools.php', ['id' => $course->id]); $PAGE->set_pagelayout('incourse'); @@ -59,4 +60,3 @@ $PAGE->requires->js_call_amd('mod_lti/course_tools_list', 'init'); echo $OUTPUT->footer(); - diff --git a/mod/lti/db/services.php b/mod/lti/db/services.php index 75e6cb1fb69ea..93c385382cbb3 100644 --- a/mod/lti/db/services.php +++ b/mod/lti/db/services.php @@ -146,6 +146,14 @@ 'ajax' => true ), + 'mod_lti_delete_course_tool_type' => array( + 'classname' => 'mod_lti\external\delete_course_tool_type', + 'description' => 'Delete a course tool type', + 'type' => 'write', + 'capabilities' => 'mod/lti:addcoursetool', + 'ajax' => true + ), + 'mod_lti_is_cartridge' => array( 'classname' => 'mod_lti_external', 'methodname' => 'is_cartridge', diff --git a/mod/lti/lang/en/lti.php b/mod/lti/lang/en/lti.php index 7746b48173e1a..7278d957821e8 100644 --- a/mod/lti/lang/en/lti.php +++ b/mod/lti/lang/en/lti.php @@ -130,6 +130,7 @@ $string['courseinformation'] = 'Course information'; $string['courselink'] = 'Go to course'; $string['coursemisconf'] = 'Course is misconfigured'; +$string['coursetooldeleted'] = 'Course tool deleted'; $string['createdon'] = 'Created on'; $string['curllibrarymissing'] = 'PHP cURL extension required for the External tool.'; $string['custom'] = 'Custom parameters'; @@ -158,6 +159,8 @@ $string['delegate_tool'] = 'As specified in Deep Linking definition or Delegate to teacher'; $string['delete'] = 'Delete'; $string['delete_confirmation'] = 'Are you sure you want to delete this preconfigured tool?'; +$string['deletecoursetool'] = 'Delete a course tool'; +$string['deletecoursetoolconfirm'] = 'Are you sure you want to delete this course tool?'; $string['deletetype'] = 'Delete preconfigured tool'; $string['display_description'] = 'Display activity description when launched'; $string['display_description_help'] = 'If selected, the activity description (specified above) will display above the tool provider\'s content. diff --git a/mod/lti/tests/behat/managecoursetools.feature b/mod/lti/tests/behat/managecoursetools.feature index 8dfea62403205..fd7791ae51dd0 100644 --- a/mod/lti/tests/behat/managecoursetools.feature +++ b/mod/lti/tests/behat/managecoursetools.feature @@ -96,3 +96,23 @@ Feature: Manage course tools And I should see "Test tool 20" in the "reportbuilder-table" "table" And I click on "2" "link" in the "page" "region" And I should see "Test tool 1" in the "reportbuilder-table" "table" + + @javascript + Scenario: Delete a course tool + Given the following "mod_lti > course tools" exist: + | name | description | baseurl | course | + | Test tool | Example description | https://example.com/tool | C1 | + | Another tool | Example 123 | https://another.example.com/tool | C1 | + And I am on the "Course 1" course page logged in as teacher1 + And I navigate to "LTI External tools" in current page administration + When I open the action menu in "Test tool" "table_row" + And I choose "Delete" in the open action menu + Then I should see "Are you sure you want to delete this course tool?" + And I click on "Cancel" "button" in the "Delete a course tool" "dialogue" + And I should see "Test tool" in the "reportbuilder-table" "table" + And I open the action menu in "Test tool" "table_row" + And I choose "Delete" in the open action menu + And I should see "Are you sure you want to delete this course tool?" + And I click on "Delete" "button" in the "Delete a course tool" "dialogue" + And I should see "Course tool deleted" + And I should not see "Test tool" in the "reportbuilder-table" "table" diff --git a/mod/lti/tests/external/delete_course_tool_type_test.php b/mod/lti/tests/external/delete_course_tool_type_test.php new file mode 100644 index 0000000000000..3d5ba8313626d --- /dev/null +++ b/mod/lti/tests/external/delete_course_tool_type_test.php @@ -0,0 +1,82 @@ +. + +namespace mod_lti\external; + +use core_external\external_api; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->dirroot . '/mod/lti/tests/mod_lti_testcase.php'); + +/** + * PHPUnit tests for delete_course_tool_type external function. + * + * @package mod_lti + * @copyright 2023 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \mod_lti\external\delete_course_tool_type + */ +class delete_course_tool_type_test extends \mod_lti_testcase { + + /** + * Test delete_course_tool() for a course tool. + * @covers ::execute + */ + public function test_delete_course_tool() { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $editingteacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); + $this->setUser($editingteacher); + + $typeid = lti_add_type( + (object) [ + 'state' => LTI_TOOL_STATE_CONFIGURED, + 'course' => $course->id + ], + (object) [ + 'lti_typename' => "My course tool", + 'lti_toolurl' => 'http://example.com', + 'lti_ltiversion' => 'LTI-1p0' + ] + ); + + $data = delete_course_tool_type::execute($typeid); + $data = external_api::clean_returnvalue(delete_course_tool_type::execute_returns(), $data); + + $this->assertTrue($data); + } + + /** + * Test delete_course_tool() for a site tool, which is forbidden. + * @covers ::execute + */ + public function test_delete_course_tool_site_tool() { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $editingteacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); + $this->setUser($editingteacher); + + $type = $this->generate_tool_type(123); // Creates a site tool. + + $this->expectException(\invalid_parameter_exception::class); + delete_course_tool_type::execute($type->id); + } +} diff --git a/mod/lti/tests/generator/lib.php b/mod/lti/tests/generator/lib.php index 171399cb65dc1..ac037ce6e777f 100644 --- a/mod/lti/tests/generator/lib.php +++ b/mod/lti/tests/generator/lib.php @@ -101,12 +101,24 @@ public function create_tool_types(array $type, ?array $config = null) { lti_add_type((object) $type, (object) $config); } - public function create_course_tool_types(array $type, ?array $config = null) { + /** + * Create a course tool type. + * + * @param array $type the type info. + * @param array|null $config the type configuration. + * @return void + * @throws coding_exception if any required fields are missing. + */ + public function create_course_tool_types(array $type, ?array $config = null): void { + global $SITE; + if (!isset($type['baseurl'])) { - throw new coding_exception('Must specify baseurl when creating a LTI tool type.'); + throw new coding_exception('Must specify baseurl when creating a course tool type.'); + } + if (!isset($type['course']) || $type['course'] == $SITE->id) { + throw new coding_exception('Must specify a non-site course when creating a course tool type.'); } $type['coursevisible'] = LTI_COURSEVISIBLE_PRECONFIGURED; // The default for course tools. lti_add_type((object) $type, (object) $config); - } } diff --git a/mod/lti/tests/mod_lti_testcase.php b/mod/lti/tests/mod_lti_testcase.php index 7d3a9af71b56a..cdd82f866c7d1 100644 --- a/mod/lti/tests/mod_lti_testcase.php +++ b/mod/lti/tests/mod_lti_testcase.php @@ -21,6 +21,7 @@ global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); +require_once($CFG->dirroot . '/mod/lti/locallib.php'); /** * Abstract base testcase for mod_lti unit tests. @@ -47,7 +48,8 @@ protected function generate_tool_type(string $uniqueid, ?int $toolproxyid = null $type->description = "Example description $uniqueid"; $type->toolproxyid = $toolproxyid; $type->baseurl = $this->getExternalTestFileUrl("/test$uniqueid.html"); - lti_add_type($type, new stdClass()); + + $type->id = lti_add_type($type, new stdClass()); return $type; } diff --git a/mod/lti/version.php b/mod/lti/version.php index 38e6946d1f7b7..6bb4402a54c39 100644 --- a/mod/lti/version.php +++ b/mod/lti/version.php @@ -48,7 +48,7 @@ defined('MOODLE_INTERNAL') || die; -$plugin->version = 2023042400; // The current module version (Date: YYYYMMDDXX). +$plugin->version = 2023070500; // The current module version (Date: YYYYMMDDXX). $plugin->requires = 2023041800; // Requires this Moodle version. $plugin->component = 'mod_lti'; // Full name of the plugin (used for diagnostics). $plugin->cron = 0;