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 00000000000..991eba8c9d2 --- /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 00000000000..13e18320ae4 --- /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 00000000000..ca234c11436 --- /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 00000000000..cd03f2c4058 --- /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/classes/output/course_tools_page.php b/mod/lti/classes/output/course_tools_page.php new file mode 100644 index 00000000000..d51f1f9910f --- /dev/null +++ b/mod/lti/classes/output/course_tools_page.php @@ -0,0 +1,77 @@ +. + +namespace mod_lti\output; + +use core_reportbuilder\system_report_factory; +use mod_lti\reportbuilder\local\systemreports\course_external_tools_list; + +/** + * The course tools page renderable, containing a page header renderable and a course tools system report. + * + * @package mod_lti + * @copyright 2023 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class course_tools_page implements \renderable { + + /** @var course_external_tools_list the course tools system report instance. */ + protected course_external_tools_list $coursetoolsreport; + + /** @var course_tools_page_header the page header renderable instance. */ + protected course_tools_page_header $coursetoolspageheader; + + /** + * Renderable constructor. + * + * @param int $courseid the id of the course. + */ + public function __construct(int $courseid) { + global $DB; + + $context = \context_course::instance($courseid); + + // Page intro, zero state and 'add new' button. + $canadd = has_capability('mod/lti:addcoursetool', $context); + $sql = 'SELECT COUNT(1) + FROM {lti_types} tt + WHERE tt.course IN(:siteid, :courseid) + AND tt.coursevisible NOT IN(:coursevisible)'; + $toolcount = $DB->count_records_sql($sql, ['siteid' => get_site()->id, 'courseid' => $courseid, 'coursevisible' => 0]); + $this->coursetoolspageheader = new course_tools_page_header($courseid, $toolcount, $canadd); + + // Course tools report itself. + $this->coursetoolsreport = system_report_factory::create(course_external_tools_list::class, $context); + } + + /** + * Get the course tools page header renderable. + * + * @return course_tools_page_header the renderable. + */ + public function get_header(): course_tools_page_header { + return $this->coursetoolspageheader; + } + + /** + * Get the course tools list system report. + * + * @return course_external_tools_list the course tools list report. + */ + public function get_table(): course_external_tools_list { + return $this->coursetoolsreport; + } +} diff --git a/mod/lti/classes/output/course_tools_page_header.php b/mod/lti/classes/output/course_tools_page_header.php new file mode 100644 index 00000000000..064248dafd3 --- /dev/null +++ b/mod/lti/classes/output/course_tools_page_header.php @@ -0,0 +1,62 @@ +. + +namespace mod_lti\output; + +use core\output\notification; +use renderer_base; + +/** + * Course tools page header renderable, containing the data for the page zero state and 'add tool' button. + * + * @package mod_lti + * @copyright 2023 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class course_tools_page_header implements \templatable { + + /** + * Constructor. + * + * @param int $courseid the course id. + * @param int $toolcount the number of tools available in the course. + * @param bool $canadd whether the user can add tools to the course or not. + */ + public function __construct(protected int $courseid, protected int $toolcount, protected bool $canadd) { + } + + /** + * Export the header's data for template use. + * + * @param renderer_base $output + * @return object the data. + */ + public function export_for_template(renderer_base $output): \stdClass { + + $context = (object) []; + + if ($this->canadd) { + $context->addlink = (new \moodle_url('/mod/lti/coursetooledit.php', ['course' => $this->courseid]))->out(); + } + + if ($this->toolcount == 0) { + $notification = new notification(get_string('nocourseexternaltoolsnotice', 'mod_lti'), notification::NOTIFY_INFO, true); + $context->notoolsnotice = $notification->export_for_template($output); + } + + return $context; + } +} diff --git a/mod/lti/classes/output/renderer.php b/mod/lti/classes/output/renderer.php index e8ba88beb6d..32354f71946 100644 --- a/mod/lti/classes/output/renderer.php +++ b/mod/lti/classes/output/renderer.php @@ -84,4 +84,20 @@ class renderer extends plugin_renderer_base { return parent::render_from_template('mod_lti/repost_crosssite', $data); } + /** + * Render the course tools page header. + * + * @param course_tools_page $page the page renderable. + * @return string the rendered html for the page. + */ + protected function render_course_tools_page(course_tools_page $page): string { + + // Render the table header templatable + the report. + $headerrenderable = $page->get_header(); + $table = $page->get_table(); + $headercontext = $headerrenderable->export_for_template($this); + $headeroutput = parent::render_from_template('mod_lti/course_tools_page_header', $headercontext); + + return $headeroutput . $table->output(); + } } diff --git a/mod/lti/classes/reportbuilder/local/entities/tool_types.php b/mod/lti/classes/reportbuilder/local/entities/tool_types.php new file mode 100644 index 00000000000..82d719cdb6b --- /dev/null +++ b/mod/lti/classes/reportbuilder/local/entities/tool_types.php @@ -0,0 +1,184 @@ +. + +namespace mod_lti\reportbuilder\local\entities; + +use core_reportbuilder\local\filters\select; +use core_reportbuilder\local\filters\text; +use lang_string; +use core_reportbuilder\local\entities\base; +use core_reportbuilder\local\report\column; +use core_reportbuilder\local\report\filter; + +/** + * Course external tools entity class implementation. + * + * Defines all the columns and filters that can be added to reports that use this entity. + * + * @package mod_lti + * @copyright 2023 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tool_types extends base { + + /** + * Database tables that this entity uses and their default aliases + * + * @return array + */ + protected function get_default_table_aliases(): array { + return ['lti_types' => 'tt', 'lti' => 'ti']; + } + + /** + * The default title for this entity + * + * @return lang_string + */ + protected function get_default_entity_title(): lang_string { + return new lang_string('entitycourseexternaltools', 'mod_lti'); + } + + /** + * Initialize the entity + * + * @return base + */ + public function initialise(): base { + $columns = $this->get_all_columns(); + foreach ($columns as $column) { + $this->add_column($column); + } + + $filters = $this->get_all_filters(); + foreach ($filters as $filter) { + $this->add_filter($filter); + } + + return $this; + } + + /** + * Returns list of all available columns + * + * @return column[] + */ + protected function get_all_columns(): array { + $tablealias = $this->get_table_alias('lti_types'); + + // Name column. + $columns[] = (new column( + 'name', + new lang_string('name', 'core'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_TEXT) + ->add_fields("{$tablealias}.name, {$tablealias}.icon") + ->set_is_sortable(true) + ->add_callback(static function(string $name, \stdClass $data) { + global $OUTPUT; + + $iconurl = $data->icon ?: $OUTPUT->image_url('monologo', 'lti')->out(); + $iconclass = $data->icon ? ' nofilter' : ''; + $iconcontainerclass = 'activityiconcontainer smaller content'; + $name = $data->name; + $img = \html_writer::img($iconurl, get_string('courseexternaltooliconalt', 'mod_lti', $name), + ['class' => 'activityicon' . $iconclass]); + $name = \html_writer::span($name, 'align-self-center'); + return \html_writer::div(\html_writer::div($img, 'mr-2 '.$iconcontainerclass) . $name, 'd-flex'); + }); + + // Description column. + $columns[] = (new column( + 'description', + new lang_string('description', 'core'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_TEXT) + ->add_field("{$tablealias}.description") + ->set_is_sortable(true); + + // Course column. + $columns[] = (new column( + 'course', + new lang_string('course', 'core'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_INTEGER) + ->add_field("{$tablealias}.course") + ->set_is_sortable(true); + + // LTI Version column. + $columns[] = (new column( + 'ltiversion', + new lang_string('version'), + $this->get_entity_name() + )) + ->add_joins($this->get_joins()) + ->set_type(column::TYPE_TEXT) + ->add_field("{$tablealias}.ltiversion") + ->set_is_sortable(true); + + return $columns; + } + + /** + * Return list of all available filters + * + * @return filter[] + */ + protected function get_all_filters(): array { + $tablealias = $this->get_table_alias('lti_types'); + + return [ + // Name filter. + (new filter( + text::class, + 'name', + new lang_string('name'), + $this->get_entity_name(), + "{$tablealias}.name" + )) + ->add_joins($this->get_joins()), + + // Description filter. + (new filter( + text::class, + 'description', + new lang_string('description'), + $this->get_entity_name(), + "{$tablealias}.description" + )) + ->add_joins($this->get_joins()), + + // LTI Version filter. + (new filter( + select::class, + 'ltiversion', + new lang_string('version'), + $this->get_entity_name(), + "{$tablealias}.ltiversion" + )) + ->add_joins($this->get_joins()) + ->set_options_callback(static function() : array { + return ['LTI-1p0' => 'Legacy LTI', '1.3.0' => "LTI Advantage"]; + }) + ]; + } +} diff --git a/mod/lti/classes/reportbuilder/local/systemreports/course_external_tools_list.php b/mod/lti/classes/reportbuilder/local/systemreports/course_external_tools_list.php new file mode 100644 index 00000000000..9d574cf9e57 --- /dev/null +++ b/mod/lti/classes/reportbuilder/local/systemreports/course_external_tools_list.php @@ -0,0 +1,192 @@ +. + +namespace mod_lti\reportbuilder\local\systemreports; + +use core_reportbuilder\local\helpers\database; +use core_reportbuilder\local\report\column; +use mod_lti\reportbuilder\local\entities\tool_types; +use core_reportbuilder\system_report; + +/** + * Course external tools list system report class implementation. + * + * @package mod_lti + * @copyright 2023 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class course_external_tools_list extends system_report { + + /** @var \stdClass the course to constrain the report to. */ + protected \stdClass $course; + + /** + * Initialise report, we need to set the main table, load our entities and set columns/filters + */ + protected function initialise(): void { + global $DB; + + $this->course = get_course($this->get_context()->instanceid); + + // Our main entity, it contains all the column definitions that we need. + $entitymain = new tool_types(); + $entitymainalias = $entitymain->get_table_alias('lti_types'); + + $this->set_main_table('lti_types', $entitymainalias); + $this->add_entity($entitymain); + + // Now we can call our helper methods to add the content we want to include in the report. + $this->add_columns($entitymain); + $this->add_filters(); + $this->add_actions(); + + // We need id and course in the actions, without entity prefixes, so add these here. + $this->add_base_fields("{$entitymainalias}.id, {$entitymainalias}.course"); + + // Scope the report to the course context only. + $paramprefix = database::generate_param_name(); + $coursevisibleparam = database::generate_param_name(); + [$insql, $params] = $DB->get_in_or_equal([get_site()->id, $this->course->id], SQL_PARAMS_NAMED, "{$paramprefix}_"); + $wheresql = "{$entitymainalias}.course {$insql} AND {$entitymainalias}.coursevisible NOT IN (:{$coursevisibleparam})"; + $params = array_merge($params, [$coursevisibleparam => 0]); + $this->add_base_condition_sql($wheresql, $params); + + $this->set_downloadable(false, get_string('pluginname', 'mod_lti')); + $this->set_default_per_page(10); + $this->set_default_no_results_notice(null); + } + + /** + * Validates access to view this report + * + * @return bool + */ + protected function can_view(): bool { + return has_capability('mod/lti:addpreconfiguredinstance', $this->get_context()); + } + + /** + * Adds the columns we want to display in the report. + * + * They are all provided by the entities we previously added in the {@see initialise} method, referencing each by their + * unique identifier + * @param tool_types $tooltypesentity + * @return void + */ + protected function add_columns(tool_types $tooltypesentity): void { + $entitymainalias = $tooltypesentity->get_table_alias('lti_types'); + + $columns = [ + 'tool_types:name', + 'tool_types:description', + ]; + + $this->add_columns_from_entities($columns); + + // Tool usage column using a custom SQL subquery to count tool instances within the course. + // TODO: This should be replaced with proper column aggregation once that's added to system_report instances in MDL-76392. + $ti = database::generate_param_name(); // Tool instance param. + $sql = "(SELECT count($ti.id) as usage + FROM {lti} $ti + WHERE $ti.typeid = {$entitymainalias}.id)"; + $this->add_column(new column( + 'usage', + new \lang_string('usage', 'mod_lti'), + $tooltypesentity->get_entity_name() + )) + ->set_type(column::TYPE_INTEGER) + ->set_is_sortable(true) + ->add_field($sql, 'usage'); + + // Attempt to create a dummy actions column, working around the limitations of the official actions feature. + $this->add_column(new column( + 'actions', new \lang_string('actions'), + $tooltypesentity->get_entity_name() + )) + ->set_type(column::TYPE_TEXT) + ->set_is_sortable(false) + ->add_fields("{$entitymainalias}.id, {$entitymainalias}.course, {$entitymainalias}.name") + ->add_callback(static function($field, $row) { + global $OUTPUT; + + // Lock actions for site-level preconfigured tools. + if (get_site()->id == $row->course) { + return \html_writer::div( + \html_writer::div( + $OUTPUT->pix_icon('t/locked', get_string('sitetoolnocourseediting', 'mod_lti') + ), 'tool-action-icon-container'), 'd-flex justify-content-end' + ); + } + + // Lock actions when the user can't add course tools. + if (!has_capability('mod/lti:addcoursetool', \context_course::instance($row->course))) { + return \html_writer::div( + \html_writer::div( + $OUTPUT->pix_icon('t/locked', get_string('courseexternaltoolsnoaddpermissions', 'mod_lti') + ), 'tool-action-icon-container'), 'd-flex justify-content-end' + ); + } + + // Build and display an action menu. + $menu = new \action_menu(); + $menu->set_menu_trigger($OUTPUT->pix_icon('i/moremenu', get_string('actions', 'core')), + 'btn btn-icon d-flex align-items-center justify-content-center'); // TODO check 'actions' lang string with UX. + + $menu->add(new \action_menu_link( + new \moodle_url('/mod/lti/coursetooledit.php', ['course' => $row->course, 'typeid' => $row->id]), + null, + get_string('edit', 'core'), + null + )); + + $menu->add(new \action_menu_link( + new \moodle_url('#'), + null, + get_string('delete', 'core'), + null, + [ + 'data-action' => 'course-tool-delete', + 'data-course-tool-id' => $row->id, + 'data-course-tool-name' => $row->name + ], + )); + + return $OUTPUT->render($menu); + }); + + // Default sorting. + $this->set_initial_sort_column('tool_types:name', SORT_ASC); + } + + /** + * Add any actions for this report. + * + * @return void + */ + protected function add_actions(): void { + } + + /** + * Adds the filters we want to display in the report + * + * They are all provided by the entities we previously added in the {@see initialise} method, referencing each by their + * unique identifier + */ + protected function add_filters(): void { + + $this->add_filters_from_entities([]); + } +} diff --git a/mod/lti/coursetooledit.php b/mod/lti/coursetooledit.php new file mode 100644 index 00000000000..d90f7456686 --- /dev/null +++ b/mod/lti/coursetooledit.php @@ -0,0 +1,90 @@ +. + +/** + * Page allowing instructors to configure course-level tools. + * + * @package mod_lti + * @copyright 2023 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use core\output\notification; + +require_once('../../config.php'); +require_once($CFG->dirroot.'/mod/lti/edit_form.php'); +require_once($CFG->dirroot.'/mod/lti/lib.php'); + +$courseid = required_param('course', PARAM_INT); +$typeid = optional_param('typeid', null, PARAM_INT); + +// Permissions etc. +require_login($courseid, false); +require_capability('mod/lti:addcoursetool', context_course::instance($courseid)); +if (!empty($typeid)) { + $type = lti_get_type($typeid); + if ($type->course != $courseid || $type->course == get_site()->id) { + throw new moodle_exception('You do not have permissions to edit this tool type.'); + } +} + +// Page setup. +$url = new moodle_url('/mod/lti/coursetooledit.php', ['courseid' => $courseid]); +$PAGE->set_url($url); +$PAGE->set_pagelayout('incourse'); +$PAGE->set_title(get_string('edittype', 'mod_lti')); // TODO confirm with UX about use of 'edit preconfigured tool' for the title. +$PAGE->set_secondary_active_tab('coursetools'); +$PAGE->add_body_class('limitedwidth'); + +$type = !empty($typeid) ? lti_get_type_type_config($typeid) : (object) ['lti_clientid' => null]; +$pageheading = !empty($typeid) ? get_string('courseexternaltooleditheading', 'mod_lti') : + get_string('courseexternaltooladdheading', 'mod_lti'); +$form = new mod_lti_edit_types_form($url, (object)array('id' => $typeid, 'clientid' => $type->lti_clientid)); + +if ($form->is_cancelled()) { + + redirect(new moodle_url('/mod/lti/coursetools.php', ['id' => $courseid])); +} else if ($data = $form->get_data()) { + + require_sesskey(); + + if (!empty($data->typeid)) { + $type = (object) ['id' => $data->typeid]; + lti_load_type_if_cartridge($data); + lti_update_type($type, $data); + $redirecturl = new moodle_url('/mod/lti/coursetools.php', ['id' => $courseid]); + $notice = get_string('courseexternaltooleditsuccess', 'mod_lti', $type->name); + } else { + $type = (object) ['state' => LTI_TOOL_STATE_CONFIGURED, 'course' => $data->course]; + lti_load_type_if_cartridge($data); + lti_add_type($type, $data); + $redirecturl = new moodle_url('/mod/lti/coursetools.php', ['id' => $courseid]); + $notice = get_string('courseexternaltooladdsuccess', 'mod_lti', $type->name); + } + + redirect($redirecturl, $notice, 0, notification::NOTIFY_SUCCESS); +} + +// Display the form. +echo $OUTPUT->header(); +echo $OUTPUT->heading($pageheading); + +if (!empty($typeid)) { + $form->set_data($type); +} +$form->display(); + +echo $OUTPUT->footer(); diff --git a/mod/lti/coursetools.php b/mod/lti/coursetools.php new file mode 100644 index 00000000000..e1192d0b633 --- /dev/null +++ b/mod/lti/coursetools.php @@ -0,0 +1,62 @@ +. + +/** + * Shows a tabulated view of all the available LTI tools in a given course. + * + * @package mod_lti + * @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"); +require_once($CFG->dirroot . '/mod/lti/lib.php'); +require_once($CFG->dirroot . '/mod/lti/locallib.php'); + +$id = required_param('id', PARAM_INT); // Course Id. + +// Access + permissions. +$course = get_course($id); +require_course_login($course, false); + +$context = context_course::instance($course->id); +if (!has_any_capability(['mod/lti:addpreconfiguredinstance', 'mod/lti:addcoursetool'], $context)) { + throw new \moodle_exception('nopermissions', 'error', '', get_string('courseexternaltoolsnoviewpermissions', 'mod_lti')); +} + +// Page setup. +global $PAGE, $OUTPUT; +$pagetitle = get_string('courseexternaltools', 'mod_lti'); +$pageurl = new moodle_url('/mod/lti/coursetools.php', ['id' => $course->id]); +$PAGE->set_pagelayout('incourse'); +$PAGE->set_context($context); +$PAGE->set_url($pageurl); +$PAGE->set_title($pagetitle); +$PAGE->set_heading(format_string($course->fullname, true, ['context' => $context])); +$PAGE->add_body_class('limitedwidth'); + +// Display. +echo $OUTPUT->header(); +echo $OUTPUT->heading($pagetitle); + +$renderer = $PAGE->get_renderer('mod_lti'); +$coursetoolspage = new course_tools_page($course->id); +echo $renderer->render($coursetoolspage); +$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 75e6cb1fb69..93c385382cb 100644 --- a/mod/lti/db/services.php +++ b/mod/lti/db/services.php @@ -146,6 +146,14 @@ $functions = array( '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 563d88f0a82..7278d957821 100644 --- a/mod/lti/lang/en/lti.php +++ b/mod/lti/lang/en/lti.php @@ -70,6 +70,7 @@ $string['add_ltiadv'] = 'Add LTI Advantage'; $string['add_ltilegacy'] = 'Add Legacy LTI'; $string['addnewapp'] = 'Enable external application'; $string['addserver'] = 'Add new trusted server'; +$string['addtool'] = 'Add tool'; $string['addtype'] = 'Add preconfigured tool'; $string['allow'] = 'Allow'; $string['allowsetting'] = 'Allow tool to store 8K of settings in Moodle'; @@ -116,10 +117,20 @@ $string['contentitem_multiple_description'] = 'The following items will be added $string['contentitem_multiple_graded'] = 'Graded activity (Maximum grade: {$a})'; $string['course_tool_types'] = 'Course tools'; $string['courseactivitiesorresources'] = 'Course activities or resources'; +$string['courseexternaltooladdheading'] = 'Add new LTI External tool'; // TODO confirm wording with UX. +$string['courseexternaltooladdsuccess'] = 'Successfully added course tool \'{$a}\'.'; // TODO confirm wording with UX. +$string['courseexternaltooleditheading'] = 'Edit LTI External tool'; // TODO confirm wording with UX. +$string['courseexternaltooleditsuccess'] = 'The changes to the course tool \'{$a}\' were saved.'; // TODO confirm wording with UX. +$string['courseexternaltooliconalt'] = 'Icon for the \'{$a}\' LTI External tool'; // TODO: confirm wording with UX. +$string['courseexternaltools'] = 'LTI External tools'; // TODO: confirm wording with UX. +$string['courseexternaltoolsinfo'] = 'LTI External tools are add-on apps you can integrate into your course, such as interactive content or assessments. Your students can access and use them without leaving your course.'; // TODO: confirm wording with UX. +$string['courseexternaltoolsnoaddpermissions'] = 'You don\'t have permission to add or edit course tools.'; // TODO: confirm wording with UX. +$string['courseexternaltoolsnoviewpermissions'] = 'View course external tools'; $string['courseid'] = 'Course ID number'; $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'; @@ -148,6 +159,8 @@ $string['delegate'] = 'Delegate to teacher'; $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. @@ -184,6 +197,7 @@ $string['enableemailnotification'] = 'Send notification emails'; $string['enableemailnotification_help'] = 'If enabled, students will receive email notification when their tool submissions are graded.'; $string['enterkeyandsecret'] = 'Enter your consumer key and shared secret'; $string['enterkeyandsecret_help'] = 'If you were given a consumer key and/or shared secret, input them here'; +$string['entitycourseexternaltools'] = 'LTI External tools'; $string['errorbadurl'] = 'URL is not a valid tool URL or cartridge.'; $string['errorincorrectconsumerkey'] = 'Consumer key is incorrect.'; $string['errorinvaliddata'] = 'Invalid data: {$a}'; @@ -339,6 +353,7 @@ $string['no_tp_configured'] = 'There are no unregistered external tool registrat $string['no_tp_pending'] = 'There are no pending external tool registrations.'; $string['no_tp_rejected'] = 'There are no rejected external tool registrations.'; $string['noattempts'] = 'No attempts have been made on this tool instance'; +$string['nocourseexternaltoolsnotice'] = 'There are no LTI external tools yet'; // TODO confirm wording with UX. $string['noltis'] = 'There are no external tool instances'; $string['noprofileservice'] = 'Profile service not found'; $string['noservers'] = 'No servers found'; @@ -507,6 +522,7 @@ $string['show_in_course_lti2_help'] = 'This tool can be shown in the activity ch $string['show_in_course_no'] = 'Do not show; use only when a matching tool URL is entered'; $string['show_in_course_preconfigured'] = 'Show as preconfigured tool when adding an external tool'; $string['size'] = 'Size parameters'; +$string['sitetoolnocourseediting'] = 'This is a site level tool which cannot be edited.'; // TODO: confirm wording with UX. $string['opensslconfiginvalid'] = 'LTI 1.3 requires a valid openssl.cnf to be configured and available to your web server. Please contact the site administrator to configure and enable openssl for this site.'; $string['submission'] = 'Submission'; $string['submissions'] = 'Submissions'; @@ -594,6 +610,7 @@ $string['unabletocreatetooltype'] = 'Unable to create tool'; $string['unabletofindtooltype'] = 'Unable to find tool for {$a->id}'; $string['unknownstate'] = 'Unknown state'; $string['update'] = 'Update'; +$string['usage'] = 'Usage'; $string['useraccountinformation'] = 'User account information'; $string['userpersonalinformation'] = 'User personal information'; $string['using_tool_cartridge'] = 'Using tool cartridge'; diff --git a/mod/lti/lib.php b/mod/lti/lib.php index c2a74c11e99..4d6951af5fb 100644 --- a/mod/lti/lib.php +++ b/mod/lti/lib.php @@ -765,3 +765,20 @@ function mod_lti_core_calendar_provide_event_action(calendar_event $event, true ); } + +/** + * Extend the course navigation with an "LTI External tools" link which redirects to a list of all tools available for course use. + * + * @param settings_navigation $navigation The settings navigation object + * @param stdClass $course The course + * @param stdclass $context Course context + * @return void + */ +function mod_lti_extend_navigation_course($navigation, $course, $context): void { + if (has_any_capability(['mod/lti:addpreconfiguredinstance', 'mod/lti:addcoursetool'], $context)) { + $url = new moodle_url('/mod/lti/coursetools.php', ['id' => $course->id]); + $settingsnode = navigation_node::create(get_string('courseexternaltools', 'mod_lti'), $url, navigation_node::TYPE_SETTING, + null, 'coursetools', new pix_icon('i/settings', '')); + $navigation->add_node($settingsnode); + } +} diff --git a/mod/lti/styles.css b/mod/lti/styles.css index ce8a451d7d1..7bd8b3690e0 100644 --- a/mod/lti/styles.css +++ b/mod/lti/styles.css @@ -403,3 +403,32 @@ border: 1px solid #ddd; border-radius: 4px; } + +/* Strip the caret from the action menu toggle, as the ... ellipsis icon is used. */ +#page-mod-lti-coursetools a.dropdown-toggle::after { + display: none; +} + +/* Icons in the 'actions' column of the course tools table need to be aligned with action menus which have a 36x36 trigger. */ +#page-mod-lti-coursetools table div.tool-action-icon-container { + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; +} + +#page-mod-lti-coursetools table div.tool-action-icon-container i { + margin: 0; +} + +/* The following removes the striping that lives in table_sql, which we can't change from within reportbuilder */ +#page-mod-lti-coursetools table.generaltable tbody tr:nth-of-type(2n+1), +#page-mod-lti-coursetools table.table-striped tbody tr:nth-of-type(2n+1) { + background: inherit; +} + +#page-mod-lti-coursetools table.generaltable tbody tr:hover, +#page-mod-lti-coursetools table.table-striped tbody tr:hover { + background: #fff; +} diff --git a/mod/lti/templates/course_tools_page_header.mustache b/mod/lti/templates/course_tools_page_header.mustache new file mode 100644 index 00000000000..9a5d0d3e26a --- /dev/null +++ b/mod/lti/templates/course_tools_page_header.mustache @@ -0,0 +1,56 @@ +{{! + 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 . +}} +{{! + @template mod_lti/course_tool_page_header + + Contains the page header for the 'LTI External tools' course page. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * notoolsnotice Object exported notification, shown when the page has 0 tools available. + * addlink String link to the add tool type page. + + Example context (json): + { + "notoolsnotice": { + "message": "There are no LTI External tools yet", + "extraclasses": "", + "announce": false, + "closebutton": true, + "isinfo": true + }, + "addlink": "http://SITE/mod/lti/coursetooledit?course=45" + } +}} +
+

+ {{#str}} courseexternaltoolsinfo, mod_lti {{/str}} +

+ {{#notoolsnotice}} + {{> core/notification_info}} + {{/notoolsnotice}} + {{#addlink}} + + {{/addlink}} +
diff --git a/mod/lti/tests/behat/managecoursetools.feature b/mod/lti/tests/behat/managecoursetools.feature new file mode 100644 index 00000000000..fd7791ae51d --- /dev/null +++ b/mod/lti/tests/behat/managecoursetools.feature @@ -0,0 +1,118 @@ +@mod @mod_lti +Feature: Manage course tools + In order to provide richer experiences for learners + As a teacher + I need to be able to add external tools to a course + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Terry1 | Teacher1 | teacher1@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + + Scenario: Create a course tool from the zero state + Given I am on the "Course 1" course page logged in as teacher1 + And I navigate to "LTI External tools" in current page administration + And I should see "LTI External tools are add-on apps" + And I should see "There are no LTI external tools yet" + When I click on "Add tool" "link" + And I press "Cancel" + Then I should see "LTI External tools are add-on apps" + And I should see "There are no LTI external tools yet" + And I click on "Add tool" "link" + And I set the following fields to these values: + | Tool name | Teaching Tool 1 | + | Tool URL | http://example.com | + | Tool description | A short description of the tool | + And I press "Save changes" + And I should see "Successfully added course tool" + And I should see "A short description of the tool" in the "Teaching Tool 1" "table_row" + + Scenario: Viewing a site level tool in the course tools table + Given the following "mod_lti > tool types" exist: + | name | description | baseurl | coursevisible | + | Example tool | Another description | https://example.com/tool1 | 0 | + | Test tool 2 | Tool2 description | https://example.com/tool2 | 1 | + | Test tool 3 | Tool3 description | https://example.com/tool3 | 2 | + And I am on the "Course 1" course page logged in as teacher1 + When I navigate to "LTI External tools" in current page administration + Then I should see "Test tool 2" in the "reportbuilder-table" "table" + And "This is a site level tool which cannot be edited" "icon" should exist in the "Test tool 2" "table_row" + And I should see "Test tool 3" in the "reportbuilder-table" "table" + And "This is a site level tool which cannot be edited" "icon" should exist in the "Test tool 3" "table_row" + And I should not see "Example tool" in the "reportbuilder-table" "table" + + Scenario: Viewing course tools without the capability to add/edit but having the capability to use + Given the following "role capability" exists: + | role | editingteacher | + | mod/lti:addcoursetool | prohibit | + | mod/lti:addpreconfiguredinstance | allow | + And the following "mod_lti > course tools" exist: + | name | description | baseurl | course | + | Test tool | Example description | https://example.com/tool | C1 | + And I am on the "Course 1" course page logged in as teacher1 + When I navigate to "LTI External tools" in current page administration + Then "You don't have permission to add or edit course tools" "icon" should exist in the "Test tool" "table_row" + + @javascript + Scenario: Edit a course tool + Given the following "mod_lti > course tools" exist: + | name | description | baseurl | course | + | Test tool | Example description | https://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 + And the "Edit" item should exist in the "Actions" action menu of the "Test tool" "table_row" + And the "Delete" item should exist in the "Actions" action menu of the "Test tool" "table_row" + When I open the action menu in "Test tool" "table_row" + And I choose "Edit" in the open action menu + And I press "Cancel" + Then I should see "Test tool" in the "reportbuilder-table" "table" + And I open the action menu in "Test tool" "table_row" + And I choose "Edit" in the open action menu + And I set the following fields to these values: + | Tool name | Test tool (edited) | + | Tool URL | http://example.com | + | Tool description | A short description of the tool (edited) | + And I press "Save changes" + And I should see "The changes to the course tool 'Test tool (edited)' were saved" + And I should see "A short description of the tool (edited)" in the "Test tool (edited)" "table_row" + + @javascript + Scenario: Navigate through the listing of course tools + Given 20 "mod_lti > course tools" exist with the following data: + | name | Test tool [count] | + | description | Example description [count] | + | baseurl | https://www.example.com/tool[count] | + | course | C1 | + And I am on the "Course 1" course page logged in as teacher1 + When I navigate to "LTI External tools" in current page administration + Then I should see "Test tool 1" in the "reportbuilder-table" "table" + And I click on "Name" "link" + 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 00000000000..3d5ba831362 --- /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/behat_mod_lti_generator.php b/mod/lti/tests/generator/behat_mod_lti_generator.php index 0f19abe86f7..b635e45aad4 100644 --- a/mod/lti/tests/generator/behat_mod_lti_generator.php +++ b/mod/lti/tests/generator/behat_mod_lti_generator.php @@ -45,6 +45,12 @@ class behat_mod_lti_generator extends behat_generator_base { 'datagenerator' => 'tool_types', 'required' => ['baseurl'], ], + 'course tools' => [ + 'singular' => 'course tool', + 'datagenerator' => 'course_tool_types', + 'required' => ['baseurl', 'course'], + 'switchids' => ['course' => 'course'] + ] ]; } } diff --git a/mod/lti/tests/generator/lib.php b/mod/lti/tests/generator/lib.php index 15f38afe1fe..ac037ce6e77 100644 --- a/mod/lti/tests/generator/lib.php +++ b/mod/lti/tests/generator/lib.php @@ -100,4 +100,25 @@ class mod_lti_generator extends testing_module_generator { } lti_add_type((object) $type, (object) $config); } + + /** + * 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 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 7d3a9af71b5..cdd82f866c7 100644 --- a/mod/lti/tests/mod_lti_testcase.php +++ b/mod/lti/tests/mod_lti_testcase.php @@ -21,6 +21,7 @@ defined('MOODLE_INTERNAL') || die(); 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 @@ abstract class mod_lti_testcase extends externallib_advanced_testcase { $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 38e6946d1f7..6bb4402a54c 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; diff --git a/reportbuilder/classes/local/report/base.php b/reportbuilder/classes/local/report/base.php index 9679c020ae8..25b0d9d4fa9 100644 --- a/reportbuilder/classes/local/report/base.php +++ b/reportbuilder/classes/local/report/base.php @@ -90,6 +90,9 @@ abstract class base { /** @var array $attributes */ private $attributes = []; + /** @var lang_string $noresultsnotice */ + private $noresultsnotice; + /** * Base report constructor * @@ -97,6 +100,7 @@ abstract class base { */ public function __construct(report $report) { $this->report = $report; + $this->noresultsnotice = new lang_string('nothingtodisplay'); // Initialise and validate the report. $this->initialise(); @@ -767,6 +771,25 @@ abstract class base { $this->defaultperpage = $defaultperpage; } + /** + * Set the default lang string for the notice used when no results are found. + * + * @param lang_string|null $notice string, or null to tell the report to omit the notice entirely. + * @return void + */ + public function set_default_no_results_notice(?lang_string $notice): void { + $this->noresultsnotice = $notice; + } + + /** + * Get the default lang string for the notice used when no results are found. + * + * @return lang_string|null the lang_string instance or null if the report prefers not to use one. + */ + public function get_default_no_results_notice(): ?lang_string { + return $this->noresultsnotice; + } + /** * Default 'per page' size * diff --git a/reportbuilder/classes/table/base_report_table.php b/reportbuilder/classes/table/base_report_table.php index fb1b892a632..a2cc69612ef 100644 --- a/reportbuilder/classes/table/base_report_table.php +++ b/reportbuilder/classes/table/base_report_table.php @@ -236,7 +236,9 @@ abstract class base_report_table extends table_sql implements dynamic, renderabl echo $this->get_dynamic_table_html_start(); echo $this->render_reset_button(); - echo $OUTPUT->render(new notification(get_string('nothingtodisplay'), notification::NOTIFY_INFO, false)); + if ($notice = $this->report->get_default_no_results_notice()) { + echo $OUTPUT->render(new notification($notice->out(), notification::NOTIFY_INFO, false)); + } echo $this->get_dynamic_table_html_end(); } diff --git a/reportbuilder/upgrade.txt b/reportbuilder/upgrade.txt index 223a0df266d..20f7ad6cb83 100644 --- a/reportbuilder/upgrade.txt +++ b/reportbuilder/upgrade.txt @@ -13,6 +13,12 @@ Information provided here is intended especially for developers. - `enrolment:method` => `enrol:plugin` * Trying to add/annotate duplicate entity names to a report will now throw a coding exception * The `get_default_entity_name` method of the base entity class is now private, and shouldn't be overridden in extending classes +* Two new methods: + - `get_default_no_results_notice` and + - `set_default_no_results_notice` + have been added to core_reportbuilder\local\report\base, allowing report implementations to control what lang string is used in + the notice when the report has no results. Reports can either set a preferred lang string, or pass null if the notice isn't + required in that particular report. === 4.2 ===