diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e624585..37333b0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ jobs: matrix: include: - php: '8.1' - moodle-branch: 'master' + moodle-branch: 'main' database: 'pgsql' - php: '8.0' moodle-branch: 'MOODLE_403_STABLE' diff --git a/amd/build/userselector.min.js b/amd/build/userselector.min.js index 04a5f85..1beef34 100644 --- a/amd/build/userselector.min.js +++ b/amd/build/userselector.min.js @@ -5,6 +5,6 @@ define("report_customsql/userselector",["exports","core/ajax","core/templates"], * @module report_customsql/userselector * @copyright 2020 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.processResults=function(selector,results){return results.map((user=>({value:user.id,label:user._label})))},_exports.transport=function(selector,query,success,failure){_ajax.default.call([{methodname:"report_customsql_get_users",args:{query:query,capability:document.getElementById("id_capability").value}}])[0].then((results=>Promise.all(results.map((user=>_templates.default.render("report_customsql/form-user-selector-suggestion",user).then((html=>(user._label=html,user)))))))).then(success).catch(failure)},_ajax=_interopRequireDefault(_ajax),_templates=_interopRequireDefault(_templates)})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.processResults=function(selector,results){return results.map((user=>({value:user.id,label:user._label})))},_exports.transport=function(selector,query,success,failure){_ajax.default.call([{methodname:"report_customsql_get_users",args:{query:query,capability:document.getElementById("id_capability").value}}])[0].then((results=>(async()=>await Promise.all(results.map((async user=>{const html=await _templates.default.render("report_customsql/form-user-selector-suggestion",user);return user._label=html,user}))))())).then(success).catch(failure)},_ajax=_interopRequireDefault(_ajax),_templates=_interopRequireDefault(_templates)})); //# sourceMappingURL=userselector.min.js.map \ No newline at end of file diff --git a/amd/build/userselector.min.js.map b/amd/build/userselector.min.js.map index 7088de6..cd110f6 100644 --- a/amd/build/userselector.min.js.map +++ b/amd/build/userselector.min.js.map @@ -1 +1 @@ -{"version":3,"file":"userselector.min.js","sources":["../src/userselector.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 * JavaScript module to work with the auto-complete of users.\n *\n * @module report_customsql/userselector\n * @copyright 2020 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\nimport Templates from 'core/templates';\n\n/**\n * Source of data for Ajax element.\n *\n * @param {String} selector The selector of the auto complete element.\n * @param {String} query The query string.\n * @param {Function} success To be called with the results, when received.\n * @param {Function} failure To be called with any errors.\n */\nexport function transport(selector, query, success, failure) {\n Ajax.call([{\n methodname: 'report_customsql_get_users',\n args: {\n query: query,\n capability: document.getElementById('id_capability').value\n }\n }])[0]\n\n .then((results) => {\n // For each user in the result, render the display, and set it on the _label field.\n return Promise.all(results.map((user) => {\n return Templates.render('report_customsql/form-user-selector-suggestion', user)\n .then((html) => {\n user._label = html;\n return user;\n });\n }));\n })\n\n .then(success)\n .catch(failure);\n}\n\n/**\n * Process the results for auto complete elements.\n *\n * @param {String} selector The selector of the auto complete element.\n * @param {Array} results An array or results.\n * @return {Array} New array of results.\n */\nexport function processResults(selector, results) {\n return results.map((user) => {\n return {\n value: user.id,\n label: user._label\n };\n });\n}\n"],"names":["selector","results","map","user","value","id","label","_label","query","success","failure","call","methodname","args","capability","document","getElementById","then","Promise","all","Templates","render","html","catch"],"mappings":";;;;;;;8FAiE+BA,SAAUC,gBAC9BA,QAAQC,KAAKC,OACT,CACHC,MAAOD,KAAKE,GACZC,MAAOH,KAAKI,wCAnCEP,SAAUQ,MAAOC,QAASC,uBAC3CC,KAAK,CAAC,CACPC,WAAY,6BACZC,KAAM,CACFL,MAAOA,MACPM,WAAYC,SAASC,eAAe,iBAAiBZ,UAEzD,GAEHa,MAAMhB,SAEIiB,QAAQC,IAAIlB,QAAQC,KAAKC,MACrBiB,mBAAUC,OAAO,iDAAkDlB,MACrEc,MAAMK,OACHnB,KAAKI,OAASe,KACPnB,aAKtBc,KAAKR,SACLc,MAAMb"} \ No newline at end of file +{"version":3,"file":"userselector.min.js","sources":["../src/userselector.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 * JavaScript module to work with the auto-complete of users.\n *\n * @module report_customsql/userselector\n * @copyright 2020 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\nimport Templates from 'core/templates';\n\n/**\n * Source of data for Ajax element.\n *\n * @param {String} selector The selector of the auto complete element.\n * @param {String} query The query string.\n * @param {Function} success To be called with the results, when received.\n * @param {Function} failure To be called with any errors.\n */\nexport function transport(selector, query, success, failure) {\n Ajax.call([{\n methodname: 'report_customsql_get_users',\n args: {\n query: query,\n capability: document.getElementById('id_capability').value\n }\n }])[0]\n\n .then((results) => {\n // For each user in the result, render the display, and set it on the _label field.\n return (async() => {\n const users = await Promise.all(results.map(async(user) => {\n const html = await Templates.render('report_customsql/form-user-selector-suggestion', user);\n user._label = html;\n return user;\n }));\n return users;\n })();\n })\n\n .then(success)\n .catch(failure);\n}\n\n/**\n * Process the results for auto complete elements.\n *\n * @param {String} selector The selector of the auto complete element.\n * @param {Array} results An array or results.\n * @return {Array} New array of results.\n */\nexport function processResults(selector, results) {\n return results.map((user) => {\n return {\n value: user.id,\n label: user._label\n };\n });\n}\n"],"names":["selector","results","map","user","value","id","label","_label","query","success","failure","call","methodname","args","capability","document","getElementById","then","Promise","all","async","html","Templates","render","catch"],"mappings":";;;;;;;8FAkE+BA,SAAUC,gBAC9BA,QAAQC,KAAKC,OACT,CACHC,MAAOD,KAAKE,GACZC,MAAOH,KAAKI,wCApCEP,SAAUQ,MAAOC,QAASC,uBAC3CC,KAAK,CAAC,CACPC,WAAY,6BACZC,KAAM,CACFL,MAAOA,MACPM,WAAYC,SAASC,eAAe,iBAAiBZ,UAEzD,GAEHa,MAAMhB,SAEI,gBACiBiB,QAAQC,IAAIlB,QAAQC,KAAIkB,MAAAA,aAClCC,WAAaC,mBAAUC,OAAO,iDAAkDpB,aACtFA,KAAKI,OAASc,KACPlB,SAJR,KAUVc,KAAKR,SACLe,MAAMd"} \ No newline at end of file diff --git a/amd/src/userselector.js b/amd/src/userselector.js index c1ccbd5..7f09730 100644 --- a/amd/src/userselector.js +++ b/amd/src/userselector.js @@ -43,13 +43,14 @@ export function transport(selector, query, success, failure) { .then((results) => { // For each user in the result, render the display, and set it on the _label field. - return Promise.all(results.map((user) => { - return Templates.render('report_customsql/form-user-selector-suggestion', user) - .then((html) => { - user._label = html; - return user; - }); - })); + return (async() => { + const users = await Promise.all(results.map(async(user) => { + const html = await Templates.render('report_customsql/form-user-selector-suggestion', user); + user._label = html; + return user; + })); + return users; + })(); }) .then(success) diff --git a/categoryadd_form.php b/categoryadd_form.php index 33bf63f..fa71dfa 100644 --- a/categoryadd_form.php +++ b/categoryadd_form.php @@ -39,7 +39,9 @@ */ class report_customsql_addcategory_form extends moodleform { - // Form definition. + /** + * Define the form. + */ public function definition() { global $CFG, $DB; $mform = $this->_form; @@ -65,6 +67,13 @@ public function definition() { $this->add_action_buttons(true, $strsubmit); } + /** + * Validation. + * + * @param array $data Form data. + * @param array $files Form files. + * @return array Array of errors. + */ public function validation($data, $files) { global $DB; $errors = parent::validation($data, $files); diff --git a/classes/event/query_deleted.php b/classes/event/query_deleted.php index 00292a2..57bdc48 100644 --- a/classes/event/query_deleted.php +++ b/classes/event/query_deleted.php @@ -32,20 +32,39 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class query_deleted extends \core\event\base { + + /** + * Event constructor. + */ protected function init() { $this->data['crud'] = 'd'; $this->data['edulevel'] = self::LEVEL_OTHER; $this->data['objecttable'] = 'report_customsql_queries'; } + /** + * Returns localised general event name. + * + * @return string + */ public static function get_name() { return get_string('query_deleted', 'report_customsql'); } + /** + * Returns description of the query deleted event. + * + * @return string + */ public function get_description() { return "User {$this->userid} has deleted the SQL query with id {$this->objectid}."; } + /** + * Returns relevant URL. + * + * @return \moodle_url + */ public function get_url() { return new \moodle_url('/report/customsql/index.php'); } diff --git a/classes/event/query_edited.php b/classes/event/query_edited.php index 89eb797..4c66119 100644 --- a/classes/event/query_edited.php +++ b/classes/event/query_edited.php @@ -32,20 +32,39 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class query_edited extends \core\event\base { + + /** + * Event constructor. + */ protected function init() { $this->data['crud'] = 'u'; $this->data['edulevel'] = self::LEVEL_OTHER; $this->data['objecttable'] = 'report_customsql_queries'; } + /** + * Returns localised general event name. + * + * @return string + */ public static function get_name() { return get_string('query_edited', 'report_customsql'); } + /** + * Returns description of the query edited event. + * + * @return string + */ public function get_description() { return "User {$this->userid} has edited the SQL query with id {$this->objectid}."; } + /** + * Returns url to view the query. + * + * @return string + */ public function get_url() { return new \moodle_url('/report/customsql/view.php', ['id' => $this->objectid]); } diff --git a/classes/event/query_viewed.php b/classes/event/query_viewed.php index a48eed9..2eaa214 100644 --- a/classes/event/query_viewed.php +++ b/classes/event/query_viewed.php @@ -32,20 +32,39 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class query_viewed extends \core\event\base { + + /** + * Event constructor. + */ protected function init() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_OTHER; $this->data['objecttable'] = 'report_customsql_queries'; } + /** + * Returns localised general event name. + * + * @return string + */ public static function get_name() { return get_string('query_viewed', 'report_customsql'); } + /** + * Returns description of the query viewed event. + * + * @return string + */ public function get_description() { return "User {$this->userid} has viewed the SQL query with id {$this->objectid}."; } + /** + * Returns relevant URL. + * + * @return \moodle_url + */ public function get_url() { return new \moodle_url('/report/customsql/view.php', ['id' => $this->objectid]); } diff --git a/classes/external/create_query.php b/classes/external/create_query.php new file mode 100644 index 0000000..aab0f7c --- /dev/null +++ b/classes/external/create_query.php @@ -0,0 +1,147 @@ +. + +namespace report_customsql\external; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->libdir . '/externallib.php'); +require_once($CFG->dirroot . '/report/customsql/edit_form.php'); + +/** + * Web service to create new queries. + * + * @package report_customsql + * @author Oscar Nadjar + * @copyright 2024 Moodle US + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class create_query extends \external_api { + /** + * Parameter declaration. + * + * @return \external_function_parameters Parameters + */ + public static function execute_parameters(): \external_function_parameters { + return new \external_function_parameters([ + 'displayname' => new \external_value(PARAM_ALPHANUMEXT, 'Short name of the query.', VALUE_REQUIRED), + 'description' => new \external_value(PARAM_RAW, 'Description of the query.', VALUE_DEFAULT, ''), + 'querysql' => new \external_value(PARAM_RAW, 'SQL query.', VALUE_REQUIRED), + 'queryparams' => new \external_value(PARAM_RAW, 'Description of the query.', VALUE_DEFAULT, ''), + 'querylimit' => new \external_value(PARAM_INT, 'Limit of the query.', VALUE_DEFAULT, 5000), + 'capability' => new \external_value(PARAM_CAPABILITY, 'Capability to view the query.', + VALUE_DEFAULT, 'moodle/site:config'), + 'runable' => new \external_value(PARAM_ALPHAEXT, 'manual, weekly, montly.', VALUE_DEFAULT, 'manual'), + 'at' => new \external_value(PARAM_TEXT, 'Time of the execution.', VALUE_DEFAULT, ''), + 'emailto' => new \external_value(PARAM_EMAIL, 'Email to send the report to.', VALUE_DEFAULT, ''), + 'emailwhat' => new \external_value(PARAM_TEXT, 'What to send in the email.', VALUE_DEFAULT, ''), + 'categoryid' => new \external_value(PARAM_INT, 'Category of the query.', VALUE_DEFAULT, 1), + 'customdir' => new \external_value(PARAM_RAW, 'Custom directory of the query.', VALUE_DEFAULT, ''), + ]); + } + + /** + * Create a new query. + * + * @param string $displayname Short name of the query. + * @param string $description Description of the query. + * @param string $querysql SQL query. + * @param string $queryparams Description of the query. + * @param int $querylimit Limit of the query. + * @param string $capability Capability to view the query. + * @param string $runable manual, weekly, montly. + * @param string $at Time of the execution. + * @param string $emailto Email to send the report to. + * @param string $emailwhat What to send in the email. + * @param int $categoryid Category of the query. + * @param string $customdir Custom directory of the query. + * + * @return int id of the created query. + */ + public static function execute( + string $displayname, + string $description, + string $querysql, + string $queryparams, + int $querylimit, + string $capability, + string $runable, + string $at, + string $emailto, + string $emailwhat, + int $categoryid, + string $customdir + ): array { + global $CFG, $DB, $USER; + + // We need an associative array in order to use the validation functions. + $params = [ + 'displayname' => $displayname, + 'description' => $description, + 'querysql' => $querysql, + 'queryparams' => $queryparams, + 'querylimit' => $querylimit, + 'capability' => $capability, + 'runable' => $runable, + 'at' => $at, + 'emailto' => $emailto, + 'emailwhat' => $emailwhat, + 'categoryid' => $categoryid, + 'customdir' => $customdir, + ]; + + // This will assign the validated values to the variables. + $formdata = self::validate_parameters(self::execute_parameters(), $params); + + // Validate the context. + $context = \context_system::instance(); + self::validate_context($context); + require_capability('report/customsql:definequeries', $context); + + // Validate the data using the form class. + $form = new \report_customsql_edit_form(); + $errors = $form->validation($formdata, []); + + if (!empty($errors)) { + throw new \moodle_exception('error', 'report_customsql', '', $errors); + } + + // We are ready to insert the query in the database. + $query = (object)$formdata; + $query->usermodified = $USER->id; + $query->timecreated = time(); + $query->timemodified = time(); + $query->id = $DB->insert_record('report_customsql_queries', $query); + + if (empty($query->id)) { + throw new \moodle_exception('error', 'report_customsql', '', $errors); + } + + return ['queryid' => $query->id]; + } + + /** + * Returns the id of the created query. + * + * @return \external_description Result type + */ + public static function execute_returns(): \external_description { + return new \external_single_structure([ + 'queryid' => new \external_value(PARAM_INT, 'id of the created query.'), + ]); + } +} diff --git a/classes/external/delete_query.php b/classes/external/delete_query.php new file mode 100644 index 0000000..83fb1b7 --- /dev/null +++ b/classes/external/delete_query.php @@ -0,0 +1,86 @@ +. + +namespace report_customsql\external; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->libdir . '/externallib.php'); + +/** + * Web service to delete a query. + * + * @package report_customsql + * @author Oscar Nadjar + * @copyright 2024 Moodle US + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class delete_query extends \external_api { + /** + * Parameter declaration. + * + * @return \external_function_parameters Parameters + */ + public static function execute_parameters(): \external_function_parameters { + return new \external_function_parameters([ + 'queryid' => new \external_value(PARAM_INT, 'The id of the query', VALUE_REQUIRED), + ]); + } + + /** + * Delete a query. + * + * @param int $queryid The id of the query. + * + * @return array + */ + public static function execute(int $queryid): array { + global $CFG, $DB, $USER; + + // This will assign the validated values to the variables. + $params = self::validate_parameters(self::execute_parameters(), ['queryid' => $queryid]); + $queryid = $params['queryid']; + + // Validate the context. + $context = \context_system::instance(); + self::validate_context($context); + require_capability('report/customsql:definequeries', $context); + + // We checkout the queryid. + if (empty($DB->record_exists('report_customsql_queries', ['id' => $queryid]))) { + throw new \moodle_exception('error:invalidqueryid', 'report_customsql'); + } + + // We delete the query. + if (empty($DB->delete_records('report_customsql_queries', ['id' => $queryid]))) { + throw new \moodle_exception('error:cannotdeletequery', 'report_customsql'); + } + + return ['success' => true]; + } + + /** + * Returns true if the query was deleted. + * + * @return \external_description Result type + */ + public static function execute_returns(): \external_description { + return new \external_single_structure([ + 'success' => new \external_value(PARAM_BOOL, 'True if the query was deleted.'), + ]); + } +} diff --git a/classes/external/get_query_results.php b/classes/external/get_query_results.php new file mode 100644 index 0000000..91d548b --- /dev/null +++ b/classes/external/get_query_results.php @@ -0,0 +1,115 @@ +. + +namespace report_customsql\external; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->libdir . '/externallib.php'); + +/** + * Web service to return the query details. + * + * @package report_customsql + * @author Oscar Nadjar + * @copyright 2024 Moodle US + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class get_query_results extends \external_api { + /** + * Parameter declaration. + * + * @return \external_function_parameters Parameters + */ + public static function execute_parameters(): \external_function_parameters { + return new \external_function_parameters([ + 'queryid' => new \external_value(PARAM_INT, 'The id of the query', VALUE_REQUIRED), + 'format' => new \external_value(PARAM_TEXT, 'The format of the file to download', VALUE_DEFAULT, 'csv'), + ]); + } + + /** + * Returns the query file results. + * + * @param int $queryid The id of the query. + * @param string $format The format of the file to download. + * @return array + */ + public static function execute(int $queryid, $format): array { + global $CFG, $DB, $USER; + + // This will assign the validated values to the variables. + $params = self::validate_parameters(self::execute_parameters(), ['queryid' => $queryid]); + $queryid = $params['queryid']; + + // Validate the context. + $context = \context_system::instance(); + self::validate_context($context); + require_capability('report/customsql:definequeries', $context); + + if (!$query = $DB->get_record('report_customsql_queries', ['id' => $queryid]) ) { + throw new \moodle_exception('error:querynotfound', 'report_customsql'); + } + // Get the files from the directory. + $resultsdir = $CFG->dataroot . "/admin_report_customsql/"; + $dataformat = class_exists('\core\dataformat_' . $format); + if (!empty($format) && !empty($dataformat)) { + throw new \moodle_exception('error:invalidformat', 'report_customsql'); + } + + $contextid = \context_system::instance()->id; + $url = new \moodle_url('/webservice/pluginfile.php/' . $contextid . '/report_customsql/download/' . $queryid . '/'); + if ($query->runable == 'manual') { + $files = glob($resultsdir . "temp/$queryid/*"); + } else { + if (!empty($query->customdir)) { + $files = glob($query->customdir . "/$queryid-*"); + } else { + $files = glob($resultsdir . "$queryid/*"); + } + } + + $response = []; + foreach ($files as $file) { + $fileinfo = pathinfo($file); + $filename = $fileinfo['filename']; + $date = !empty($query->customdir) ? str_replace($queryid . '-', '', $filename) : $filename; + $humandate = date('Y-m-d H:i:s', $date); + if (empty($humandate)) { + throw new \moodle_exception('error:invaliddate', 'report_customsql'); + } + $donwloadurl = new \moodle_url($url, ['dataformat' => $format, 'timestamp' => $date]); + $response[] = ['date' => $humandate, 'downloadurl' => $donwloadurl->out(false)]; + } + + return ['results' => $response]; + } + + /** + * Returns the query details if exists. + * + * @return \external_description Result type + */ + public static function execute_returns(): \external_description { + return new \external_single_structure([ + 'results' => new \external_multiple_structure(new \external_single_structure([ + 'date' => new \external_value(PARAM_TEXT, 'The date of the report.'), + 'downloadurl' => new \external_value(PARAM_URL, 'The download URL of the file.'), + ])), + ]); + } +} diff --git a/classes/external/list_queries.php b/classes/external/list_queries.php new file mode 100644 index 0000000..52771e7 --- /dev/null +++ b/classes/external/list_queries.php @@ -0,0 +1,96 @@ +. + +namespace report_customsql\external; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->libdir . '/externallib.php'); + +/** + * Web service to list the queries. + * + * @package report_customsql + * @author Oscar Nadjar + * @copyright 2024 Moodle US + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class list_queries extends \external_api { + /** + * Parameter declaration. + * + * @return \external_function_parameters Parameters + */ + public static function execute_parameters(): \external_function_parameters { + return new \external_function_parameters([ + 'page' => new \external_value(PARAM_INT, 'Page number', VALUE_DEFAULT, 1), + 'pagesize' => new \external_value(PARAM_INT, 'The pagesize', VALUE_DEFAULT, 20), + ]); + } + + /** + * Delete a query. + * + * @param int $page The page number. + * @param int $pagesize The pagesize. + * + * @return array + */ + public static function execute(int $page, int $pagesize): array { + global $CFG, $DB, $USER; + + // This will assign the validated values to the variables. + $params = self::validate_parameters(self::execute_parameters(), ['page' => $page, 'pagesize' => $pagesize]); + $page = !empty($params['page']) ? $params['page'] : 1; + $pagesize = !empty($params['pagesize']) ? $params['pagesize'] : 20; + + // Validate the context. + $context = \context_system::instance(); + self::validate_context($context); + require_capability('report/customsql:definequeries', $context); + $fields = 'id,displayname'; + $page = $page < 1 ? 0 : $page - 1; + $queries = $DB->get_records('report_customsql_queries', [], '', $fields, $page * $pagesize, $pagesize); + $totalqueries = $DB->count_records('report_customsql_queries'); + $totalpages = ceil($totalqueries / $pagesize); + if ($page > $totalpages) { + throw new \moodle_exception('invalidpagenumber', 'report_customsql', '', + ['page' => $page, 'totalpages' => $totalpages]); + } + + return ['page' => $page + 1, 'totalpages' => $totalpages, 'pagesize' => $pagesize, 'queries' => $queries]; + } + + /** + * Returns the queries paginated. + * + * @return \external_description Result type + */ + public static function execute_returns(): \external_description { + return new \external_single_structure([ + 'page' => new \external_value(PARAM_INT, 'True if the query was deleted.'), + 'totalpages' => new \external_value(PARAM_INT, 'True if the query was deleted.'), + 'queries' => new \external_multiple_structure( + new \external_single_structure([ + 'id' => new \external_value(PARAM_INT, 'The id of the query.'), + 'displayname' => new \external_value(PARAM_TEXT, 'The display name of the query.'), + ]), + 'The list of queries' + ), + ]); + } +} diff --git a/classes/external/query_details.php b/classes/external/query_details.php new file mode 100644 index 0000000..a4b54b1 --- /dev/null +++ b/classes/external/query_details.php @@ -0,0 +1,92 @@ +. + +namespace report_customsql\external; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->libdir . '/externallib.php'); + +/** + * Web service to return the query details. + * + * @package report_customsql + * @author Oscar Nadjar + * @copyright 2024 Moodle US + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class query_details extends \external_api { + /** + * Parameter declaration. + * + * @return \external_function_parameters Parameters + */ + public static function execute_parameters(): \external_function_parameters { + return new \external_function_parameters([ + 'queryid' => new \external_value(PARAM_INT, 'The id of the query', VALUE_REQUIRED), + ]); + } + + /** + * Returns the query details. + * + * @param int $queryid The id of the query. + * + * @return array + */ + public static function execute(int $queryid): array { + global $CFG, $DB, $USER; + + // This will assign the validated values to the variables. + $params = self::validate_parameters(self::execute_parameters(), ['queryid' => $queryid]); + $queryid = $params['queryid']; + + // Validate the context. + $context = \context_system::instance(); + self::validate_context($context); + require_capability('report/customsql:definequeries', $context); + + $query = $DB->get_record('report_customsql_queries', ['id' => $queryid], '*', MUST_EXIST); + + return ['query' => $query]; + } + + /** + * Returns the query details if exists. + * + * @return \external_description Result type + */ + public static function execute_returns(): \external_description { + return new \external_single_structure([ + 'query' => new \external_single_structure([ + 'id' => new \external_value(PARAM_INT, 'The id of the query.'), + 'displayname' => new \external_value(PARAM_TEXT, 'The display name of the query.'), + 'description' => new \external_value(PARAM_RAW, 'The description of the query.'), + 'querysql' => new \external_value(PARAM_RAW, 'The SQL query.'), + 'queryparams' => new \external_value(PARAM_TEXT, 'The description of the query.'), + 'querylimit' => new \external_value(PARAM_INT, 'The limit of the query.'), + 'capability' => new \external_value(PARAM_CAPABILITY, 'The capability to view the query.'), + 'runable' => new \external_value(PARAM_ALPHAEXT, 'The runable of the query.'), + 'at' => new \external_value(PARAM_TEXT, 'The time of the execution.'), + 'emailto' => new \external_value(PARAM_EMAIL, 'The email to send the report to.'), + 'emailwhat' => new \external_value(PARAM_TEXT, 'The what to send in the email.'), + 'categoryid' => new \external_value(PARAM_INT, 'The category of the query.'), + 'customdir' => new \external_value(PARAM_TEXT, 'The custom directory of the query.'), + ]), + ]); + } +} diff --git a/classes/external/query_validation.php b/classes/external/query_validation.php new file mode 100644 index 0000000..00ffc0f --- /dev/null +++ b/classes/external/query_validation.php @@ -0,0 +1,94 @@ +. + +namespace report_customsql\external; +use external_multiple_structure; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->libdir . '/externallib.php'); +require_once($CFG->dirroot . '/report/customsql/locallib.php'); + +/** + * Web service to validate a query. + * + * @package report_customsql + * @author Oscar Nadjar + * @copyright 2024 Moodle US + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class query_validation extends \external_api { + /** + * Parameter declaration. + * + * @return \external_function_parameters Parameters + */ + public static function execute_parameters(): \external_function_parameters { + return new \external_function_parameters([ + 'queryid' => new \external_value(PARAM_INT, 'The id of the query', VALUE_REQUIRED), + 'rowlimit' => new \external_value(PARAM_INT, 'The limit of rows', VALUE_DEFAULT, 10), + ]); + } + + /** + * Delete a query. + * + * @param int $queryid The id of the query. + * + * @return array + */ + public static function execute(int $queryid): array { + global $CFG, $DB, $USER; + + // This will assign the validated values to the variables. + $params = self::validate_parameters(self::execute_parameters(), ['queryid' => $queryid]); + $queryid = $params['queryid']; + $rowlimit = $params['rowlimit']; + $limittestrows = get_config('report_customsql', 'limittestrows'); + $limittestrows = $limittestrows < 100 ? $limittestrows : 100; + + // Validate the context. + $context = \context_system::instance(); + self::validate_context($context); + require_capability('report/customsql:definequeries', $context); + + // We checkout the queryid. + $query = $DB->get_record('report_customsql_queries', ['id' => $queryid], '*', MUST_EXIST); + $sql = report_customsql_prepare_sql($query, time()); + $limit = !empty($limittestrows) ? $limittestrows : $rowlimit; + $queryparams = !empty($query->queryparams) ? unserialize($query->queryparams) : null; + $rs = report_customsql_execute_query($sql, $queryparams, $limit); + $result = []; + foreach ($rs as $row) { + $result[] = $row; + } + $rs->close(); + return ['result' => json_encode($result)]; + } + + /** + * Returns results if the query is valid to be executed. + * + * @return \external_description Result type + */ + public static function execute_returns(): \external_description { + return new \external_single_structure([ + 'result' => new \external_value(PARAM_RAW, 'The result of the query json formated', VALUE_DEFAULT, null), + ] + ); + } +} diff --git a/classes/external/update_query.php b/classes/external/update_query.php new file mode 100644 index 0000000..270443f --- /dev/null +++ b/classes/external/update_query.php @@ -0,0 +1,158 @@ +. + +namespace report_customsql\external; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->libdir . '/externallib.php'); +require_once($CFG->dirroot . '/report/customsql/edit_form.php'); + +/** + * Web service to update a query. + * + * @package report_customsql + * @author Oscar Nadjar + * @copyright 2024 Moodle US + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class update_query extends \external_api { + /** + * Parameter declaration. + * + * @return \external_function_parameters Parameters + */ + public static function execute_parameters(): \external_function_parameters { + return new \external_function_parameters([ + 'queryid' => new \external_value(PARAM_INT, 'id of the query.', VALUE_REQUIRED), + 'displayname' => new \external_value(PARAM_ALPHANUMEXT, 'Short name of the query.', VALUE_DEFAULT, ''), + 'description' => new \external_value(PARAM_RAW, 'Description of the query.', VALUE_DEFAULT, ''), + 'querysql' => new \external_value(PARAM_RAW, 'SQL query.', VALUE_DEFAULT, ''), + 'queryparams' => new \external_value(PARAM_RAW, 'Description of the query updated', VALUE_DEFAULT, ''), + 'querylimit' => new \external_value(PARAM_INT, 'Limit of the query updated.', VALUE_DEFAULT, 5000), + 'capability' => new \external_value(PARAM_CAPABILITY, 'Capability to view the query updated.', + VALUE_DEFAULT, 'moodle/site:config'), + 'runable' => new \external_value(PARAM_ALPHAEXT, 'manual, weekly, montly.', VALUE_DEFAULT, 'manual'), + 'at' => new \external_value(PARAM_TEXT, 'Time of the execution updated.', VALUE_DEFAULT, ''), + 'emailto' => new \external_value(PARAM_EMAIL, 'Email to send the report to updated.', VALUE_DEFAULT, ''), + 'emailwhat' => new \external_value(PARAM_TEXT, 'What to send in the email updated.', VALUE_DEFAULT, ''), + 'categoryid' => new \external_value(PARAM_INT, 'Category of the query updated.', VALUE_DEFAULT, 1), + 'customdir' => new \external_value(PARAM_RAW, 'Custom directory of the query updated.', VALUE_DEFAULT, ''), + ]); + } + + /** + * Update a query. + * + * @param int $queryid Id of the query. + * @param string $displayname Short name of the query. + * @param string $description Description of the query. + * @param string $querysql SQL query. + * @param string $queryparams Description of the query. + * @param int $querylimit Limit of the query. + * @param string $capability Capability to view the query. + * @param string $runable manual, weekly, montly. + * @param string $at Time of the execution. + * @param string $emailto Email to send the report to. + * @param string $emailwhat What to send in the email. + * @param int $categoryid Category of the query. + * @param string $customdir Custom directory of the query. + * + * @return array + */ + public static function execute( + int $queryid, + string $displayname, + string $description, + string $querysql, + string $queryparams, + int $querylimit, + string $capability, + string $runable, + string $at, + string $emailto, + string $emailwhat, + int $categoryid, + string $customdir + ): array { + global $CFG, $DB, $USER; + + // We need an associative array in order to use the validation functions. + $params = [ + 'queryid' => $queryid, + 'displayname' => $displayname, + 'description' => $description, + 'querysql' => $querysql, + 'queryparams' => $queryparams, + 'querylimit' => $querylimit, + 'capability' => $capability, + 'runable' => $runable, + 'at' => $at, + 'emailto' => $emailto, + 'emailwhat' => $emailwhat, + 'categoryid' => $categoryid, + 'customdir' => $customdir, + ]; + + // We checkout the parameters. + self::validate_parameters(self::execute_parameters(), $params); + + // We checkout the queryid. + if (empty($DB->record_exists('report_customsql_queries', ['id' => $queryid]))) { + throw new \moodle_exception('error:invalidqueryid', 'report_customsql'); + } + + // Validate the context. + $context = \context_system::instance(); + self::validate_context($context); + require_capability('report/customsql:definequeries', $context); + + // We update the query. + $query = $DB->get_record('report_customsql_queries', ['id' => $queryid], '*'); + $query->displayname = !empty($displayname) ? $displayname : $query->displayname; + $query->description = !empty($description) ? $description : $query->description; + $query->querysql = !empty($querysql) ? $querysql : $query->querysql; + $query->queryparams = !empty($queryparams) ? $queryparams : $query->queryparams; + $query->querylimit = !empty($querylimit) ? $querylimit : $query->querylimit; + $query->capability = !empty($capability) ? $capability : $query->capability; + $query->runable = !empty($runable) ? $runable : $query->runable; + $query->at = !empty($at) ? $at : $query->at; + $query->emailto = !empty($emailto) ? $emailto : $query->emailto; + $query->emailwhat = !empty($emailwhat) ? $emailwhat : $query->emailwhat; + $query->categoryid = !empty($categoryid) ? $categoryid : $query->categoryid; + $query->customdir = !empty($customdir) ? $customdir : $query->customdir; + $query->usermodified = $USER->id; + $query->timemodified = time(); + + if (empty($DB->update_record('report_customsql_queries', $query))) { + throw new \moodle_exception('error:updatefail', 'report_customsql', ''); + } + + return ['success' => true]; + } + + /** + * Returns true if the query was successfully updated. + * + * @return \external_description Result type + */ + public static function execute_returns(): \external_description { + return new \external_single_structure([ + 'success' => new \external_value(PARAM_BOOL, 'Succes of the update.'), + ]); + } +} diff --git a/classes/local/query.php b/classes/local/query.php index 66c5f35..099bc88 100644 --- a/classes/local/query.php +++ b/classes/local/query.php @@ -70,7 +70,7 @@ public function get_url(): moodle_url { * @param moodle_url|null $returnurl Return url. * @return moodle_url Edit url. */ - public function get_edit_url(moodle_url $returnurl = null): moodle_url { + public function get_edit_url(moodle_url | null $returnurl = null): moodle_url { $param = ['id' => $this->record->id]; if ($returnurl) { $param['returnurl'] = $returnurl->out_as_local_url(false); @@ -85,7 +85,7 @@ public function get_edit_url(moodle_url $returnurl = null): moodle_url { * @param moodle_url|null $returnurl Return url. * @return moodle_url Delete url. */ - public function get_delete_url(moodle_url $returnurl = null): moodle_url { + public function get_delete_url(moodle_url | null $returnurl = null): moodle_url { $param = ['id' => $this->record->id]; if ($returnurl) { $param['returnurl'] = $returnurl->out_as_local_url(false); @@ -118,7 +118,6 @@ public function get_capability_string() { * * @param \context $context The context to check. * @return bool true if the user has this capability. Otherwise false. - * @covers \report_customsql\local\query */ public function can_edit(\context $context): bool { return has_capability('report/customsql:definequeries', $context); @@ -130,7 +129,7 @@ public function can_edit(\context $context): bool { * @param \context $context The context to check. * @return bool Has capability to view or not? */ - public function can_view(\context $context):bool { + public function can_view(\context $context): bool { return empty($report->capability) || has_capability($report->capability, $context); } } diff --git a/classes/output/category.php b/classes/output/category.php index 67090c8..0688286 100644 --- a/classes/output/category.php +++ b/classes/output/category.php @@ -68,7 +68,7 @@ class category implements renderable, templatable { * @param moodle_url|null $returnurl Return url. */ public function __construct(report_category $category, context $context, bool $expandable = false, int $showcat = 0, - int $hidecat = 0, bool $showonlythislink = false, bool $addnewquerybtn = true, moodle_url $returnurl = null) { + int $hidecat = 0, bool $showonlythislink = false, bool $addnewquerybtn = true, moodle_url | null $returnurl = null) { $this->category = $category; $this->context = $context; $this->expandable = $expandable; @@ -79,6 +79,12 @@ public function __construct(report_category $category, context $context, bool $e $this->returnurl = $returnurl ?? $this->category->get_url(); } + /** + * Export data for template. + * + * @param renderer_base $output + * @return array + */ public function export_for_template(renderer_base $output) { $queriesdata = $this->category->get_queries_data(); diff --git a/classes/output/category_query.php b/classes/output/category_query.php index 9a0b67c..6983ab2 100644 --- a/classes/output/category_query.php +++ b/classes/output/category_query.php @@ -59,6 +59,12 @@ public function __construct(query $query, category $category, context $context, $this->returnurl = $returnurl; } + /** + * Export data for template. + * + * @param \renderer_base $output + * @return array + */ public function export_for_template(\renderer_base $output) { $imgedit = $output->pix_icon('t/edit', get_string('edit')); $imgdelete = $output->pix_icon('t/delete', get_string('delete')); diff --git a/classes/output/index_page.php b/classes/output/index_page.php index b84fa80..9c6e8bc 100644 --- a/classes/output/index_page.php +++ b/classes/output/index_page.php @@ -69,6 +69,12 @@ public function __construct(array $categories, array $queries, context $context, $this->hidecat = $hidecat; } + /** + * Export data for template. + * + * @param renderer_base $output + * @return array + */ public function export_for_template(renderer_base $output) { $categoriesdata = []; $grouppedqueries = utils::group_queries_by_category($this->queries); diff --git a/classes/output/renderer.php b/classes/output/renderer.php index a33dbae..f60d53b 100644 --- a/classes/output/renderer.php +++ b/classes/output/renderer.php @@ -39,7 +39,7 @@ class renderer extends plugin_renderer_base { * @param context $context context to use for permission checks. * @return string HTML for report actions. */ - public function render_report_actions(stdClass $report, stdClass $category, context $context):string { + public function render_report_actions(stdClass $report, stdClass $category, context $context): string { $editaction = null; $deleteaction = null; if (has_capability('report/customsql:definequeries', $context)) { diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php index 4af2b26..ec0d4a7 100644 --- a/classes/privacy/provider.php +++ b/classes/privacy/provider.php @@ -13,6 +13,7 @@ // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . + /** * Privacy Subsystem implementation for report_customsql. * diff --git a/classes/utils.php b/classes/utils.php index dcb27b4..f23dd62 100644 --- a/classes/utils.php +++ b/classes/utils.php @@ -58,8 +58,14 @@ public static function group_queries_by_category($queries) { return $grouppedqueries; } + /** + * Get queries data. + * + * @param array $queries Array of queries. + * @return bool + */ public function get_queries_data($queries) { - + return false; } /** diff --git a/db/services.php b/db/services.php index 4006ffc..1593376 100644 --- a/db/services.php +++ b/db/services.php @@ -34,4 +34,88 @@ 'type' => 'read', 'ajax' => true, ], + 'report_customsql_create_query' => [ + 'classname' => 'report_customsql\external\create_query', + 'methodname' => 'execute', + 'classpath' => '', + 'description' => 'Use to create a new query.', + 'capabilities' => 'report/customsql:definequeries', + 'type' => 'read', + 'ajax' => true, + ], + 'report_customsql_update_query' => [ + 'classname' => 'report_customsql\external\update_query', + 'methodname' => 'execute', + 'classpath' => '', + 'description' => 'Use to update a query.', + 'capabilities' => 'report/customsql:definequeries', + 'type' => 'read', + 'ajax' => true, + ], + 'report_customsql_delete_query' => [ + 'classname' => 'report_customsql\external\delete_query', + 'methodname' => 'execute', + 'classpath' => '', + 'description' => 'Use to delete a query.', + 'capabilities' => 'report/customsql:definequeries', + 'type' => 'read', + 'ajax' => true, + ], + 'report_customsql_list_queries' => [ + 'classname' => 'report_customsql\external\list_queries', + 'methodname' => 'execute', + 'classpath' => '', + 'description' => 'Use to list the sql queries.', + 'capabilities' => 'report/customsql:definequeries', + 'type' => 'read', + 'ajax' => true, + ], + 'report_customsql_query_details' => [ + 'classname' => 'report_customsql\external\query_details', + 'methodname' => 'execute', + 'classpath' => '', + 'description' => 'Use to get the details of a query.', + 'capabilities' => 'report/customsql:definequeries', + 'type' => 'read', + 'ajax' => true, + ], + 'report_customsql_get_query_results' => [ + 'classname' => 'report_customsql\external\get_query_results', + 'methodname' => 'execute', + 'classpath' => '', + 'description' => 'Use to get the results of a query.', + 'capabilities' => 'report/customsql:definequeries', + 'type' => 'read', + 'ajax' => true, + ], + 'report_customsql_query_validation' => [ + 'classname' => 'report_customsql\external\query_validation', + 'methodname' => 'execute', + 'classpath' => '', + 'description' => 'Use to validate a query.', + 'capabilities' => 'report/customsql:definequeries', + 'type' => 'read', + 'ajax' => true, + ], +]; + +$services = [ + 'report_customsql_service' => [ + 'functions' => [ + 'report_customsql_get_users', + 'report_customsql_create_query', + 'report_customsql_update_query', + 'report_customsql_delete_query', + 'report_customsql_list_queries', + 'report_customsql_query_details', + 'report_customsql_get_query_results', + 'report_customsql_query_validation', + ], + 'requiredcapability' => '', + 'restrictedusers' => 0, + 'enabled' => 1, + 'shortname' => 'customsqlws', + 'downloadfiles' => 1, + 'uploadfiles' => 0, + ], ]; diff --git a/edit_form.php b/edit_form.php index a0f9f5c..eedc468 100644 --- a/edit_form.php +++ b/edit_form.php @@ -34,6 +34,10 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class report_customsql_edit_form extends moodleform { + + /** + * Define the form. + */ public function definition() { global $CFG; @@ -43,7 +47,7 @@ public function definition() { $categoryoptions = report_customsql_category_options(); $mform->addElement('select', 'categoryid', get_string('category', 'report_customsql'), $categoryoptions); - if ($customdata['forcecategoryid'] && array_key_exists($customdata['forcecategoryid'], $categoryoptions)) { + if (!empty($customdata['forcecategoryid']) && array_key_exists($customdata['forcecategoryid'], $categoryoptions)) { $catdefault = $customdata['forcecategoryid']; } else { $catdefault = isset($categoryoptions[1]) ? 1 : key($categoryoptions); @@ -70,7 +74,7 @@ public function definition() { $mform->registerNoSubmitButton('verify'); $hasparameters = 0; - if ($customdata['queryparams']) { + if (!empty($customdata['queryparams'])) { $mform->addElement('static', 'params', '', get_string('queryparams', 'report_customsql')); foreach ($customdata['queryparams'] as $queryparam => $formparam) { $type = report_customsql_get_element_type($queryparam); @@ -155,6 +159,11 @@ public function definition() { $this->add_action_buttons(); } + /** + * Set the form data. + * + * @param stdClass $currentvalues + */ public function set_data($currentvalues) { global $DB, $OUTPUT; @@ -181,6 +190,13 @@ public function set_data($currentvalues) { $mform->addElement('html', $reportinfo); } + /** + * Validate the form data. + * + * @param array $data + * @param array $files + * @return array + */ public function validation($data, $files) { global $CFG, $DB, $USER; @@ -262,7 +278,7 @@ public function validation($data, $files) { // Check querylimit is in range. $maxlimit = get_config('report_customsql', 'querylimitmaximum'); - if (empty($data['querylimit']) || $data['querylimit'] > $maxlimit) { + if ($data['querylimit'] > $maxlimit) { $errors['querylimit'] = get_string('querylimitrange', 'report_customsql', $maxlimit); } diff --git a/lang/en/report_customsql.php b/lang/en/report_customsql.php index 26ad086..eb0f4f2 100644 --- a/lang/en/report_customsql.php +++ b/lang/en/report_customsql.php @@ -35,8 +35,8 @@ $string['automaticallyweekly'] = 'Scheduled, on the first day of each week'; $string['availablereports'] = 'On-demand queries'; $string['availableto'] = 'Available to {$a}.'; -$string['backtoreportlist'] = 'Back to the list of queries'; $string['backtocategory'] = 'Back to category \'{$a}\''; +$string['backtoreportlist'] = 'Back to the list of queries'; $string['category'] = 'Category'; $string['categorycontent'] = '({$a->manual} on-demand, {$a->daily} daily, {$a->weekly} weekly, {$a->monthly} monthly)'; $string['categoryexists'] = 'Category names must be unique, this name already exists'; @@ -62,8 +62,8 @@ $string['deletereportx'] = 'Delete query \'{$a}\''; $string['description'] = 'Description'; $string['displayname'] = 'Query name'; -$string['displaynamex'] = 'Query name: {$a}'; $string['displaynamerequired'] = 'You must enter a query name'; +$string['displaynamex'] = 'Query name: {$a}'; $string['downloadthisreportas'] = 'Download these results as'; $string['downloadthisreportascsv'] = 'Download these results as CSV'; $string['edit'] = 'Add/Edit'; @@ -71,9 +71,10 @@ $string['editcategoryx'] = 'Edit category \'{$a}\''; $string['editingareport'] = 'Editing an ad-hoc database query'; $string['editreportx'] = 'Edit query \'{$a}\''; +$string['emailbody'] = 'Dear {$a}'; +$string['emailink'] = 'To access the report, click this link: {$a}'; $string['emailnumberofrows'] = 'Just the number of rows and the link'; $string['emailresults'] = 'Put the results in the email body'; -$string['emailink'] = 'To access the report, click this link: {$a}'; $string['emailrow'] = 'The report returned {$a} row.'; $string['emailrows'] = 'The report returned {$a} rows.'; $string['emailsent'] = 'An email notification has been sent to {$a}'; @@ -82,21 +83,25 @@ $string['emailsubject1row'] = 'Query {$a->name} [1 row] [{$a->env}]'; $string['emailsubjectnodata'] = 'Query {$a->name} [no results] [{$a->env}]'; $string['emailsubjectxrows'] = 'Query {$a->name} [{$a->rows} rows] [{$a->env}]'; -$string['emailbody'] = 'Dear {$a}'; $string['emailto'] = 'Automatically email to'; $string['emailwhat'] = 'What to email'; $string['enterparameters'] = 'Enter parameters for ad-hoc database query'; +$string['error:cannotdeletequery'] = 'Error: cannot delete query'; +$string['error:invalidqueryid'] = 'Invalid query id'; +$string['error:updatefail'] = 'Update failed'; $string['errordeletingcategory'] = '

Error deleting a query category.

It must be empty to delete it.

'; $string['errordeletingreport'] = 'Error deleting a query.'; $string['errorinsertingreport'] = 'Error inserting a query.'; $string['errorupdatingreport'] = 'Error updating a query.'; $string['invalidreportid'] = 'Invalid query id {$a}.'; $string['lastexecuted'] = 'This query was last run on {$a->lastrun}. It took {$a->lastexecutiontime}s to run.'; -$string['messageprovider:notification'] = 'Ad-hoc database query notifications'; +$string['limittestrows'] = 'Maximum allowed limit on rows on the ws Query validations'; +$string['limittestrows_desc'] = 'This is the limit on the Query validation WS(Up to 99).'; $string['managecategories'] = 'Manage report categories'; $string['manual'] = 'On-demand'; $string['manualheader'] = 'On-demand'; $string['manualheader_help'] = 'These queries are run on-demand, when you click the link to view the results.'; +$string['messageprovider:notification'] = 'Ad-hoc database query notifications'; $string['monthlyheader'] = 'Monthly'; $string['monthlyheader_help'] = 'These queries are automatically run on the first day of each month, to report on the previous month. These links let you view the results that has already been accumulated.'; $string['monthlynote_help'] = 'These queries are automatically run on the first day of each month, to report on the previous month. These links let you view the results that has already been accumulated.'; @@ -114,28 +119,29 @@ $string['onerow'] = 'The query returns one row, accumulate the results one row at a time'; $string['parametervalue'] = '{$a->name}: {$a->value}'; $string['pluginname'] = 'Ad-hoc database queries'; +$string['privacy:metadata'] = 'The Ad-hoc database queries plugin does not store any personal data.'; $string['privacy:metadata:reportcustomsqlqueries'] = 'Ad-hoc database queries'; -$string['privacy:metadata:reportcustomsqlqueries:displayname'] = 'The name of the report as displayed in the UI'; +$string['privacy:metadata:reportcustomsqlqueries:at'] = 'The time for the daily report'; +$string['privacy:metadata:reportcustomsqlqueries:capability'] = 'The capability that a user needs to have to run this report'; +$string['privacy:metadata:reportcustomsqlqueries:categoryid'] = 'The category ID from report_customsql_categories table'; +$string['privacy:metadata:reportcustomsqlqueries:customdir'] = 'Export csv report to path / directory'; $string['privacy:metadata:reportcustomsqlqueries:description'] = 'A human-readable description of the query.'; $string['privacy:metadata:reportcustomsqlqueries:descriptionformat'] = 'Query description text format'; -$string['privacy:metadata:reportcustomsqlqueries:querysql'] = 'The SQL to run to generate this report'; -$string['privacy:metadata:reportcustomsqlqueries:queryparams'] = 'The SQL parameters to generate this report'; -$string['privacy:metadata:reportcustomsqlqueries:querylimit'] = 'Limit the number of results returned'; -$string['privacy:metadata:reportcustomsqlqueries:capability'] = 'The capability that a user needs to have to run this report'; -$string['privacy:metadata:reportcustomsqlqueries:lastrun'] = 'When this report was last run'; +$string['privacy:metadata:reportcustomsqlqueries:displayname'] = 'The name of the report as displayed in the UI'; +$string['privacy:metadata:reportcustomsqlqueries:emailto'] = 'A comma-separated list of user ids'; +$string['privacy:metadata:reportcustomsqlqueries:emailwhat'] = 'A list of email options in a select menu'; $string['privacy:metadata:reportcustomsqlqueries:lastexecutiontime'] = 'Time this report took to run last time it was executed, in milliseconds'; +$string['privacy:metadata:reportcustomsqlqueries:lastrun'] = 'When this report was last run'; +$string['privacy:metadata:reportcustomsqlqueries:querylimit'] = 'Limit the number of results returned'; +$string['privacy:metadata:reportcustomsqlqueries:queryparams'] = 'The SQL parameters to generate this report'; +$string['privacy:metadata:reportcustomsqlqueries:querysql'] = 'The SQL to run to generate this report'; $string['privacy:metadata:reportcustomsqlqueries:runable'] = 'Runable \'manual\', \'weekly\' or \'monthly\''; $string['privacy:metadata:reportcustomsqlqueries:singlerow'] = 'Only meaningful to set this scheduled reports. Means the report can only return one row of data, and the report builds up a row at a time'; -$string['privacy:metadata:reportcustomsqlqueries:at'] = 'The time for the daily report'; -$string['privacy:metadata:reportcustomsqlqueries:emailto'] = 'A comma-separated list of user ids'; -$string['privacy:metadata:reportcustomsqlqueries:emailwhat'] = 'A list of email options in a select menu'; -$string['privacy:metadata:reportcustomsqlqueries:categoryid'] = 'The category ID from report_customsql_categories table'; -$string['privacy:metadata:reportcustomsqlqueries:customdir'] = 'Export csv report to path / directory'; -$string['privacy:metadata:reportcustomsqlqueries:usermodified'] = 'User modified'; $string['privacy:metadata:reportcustomsqlqueries:timecreated'] = 'Time created'; $string['privacy:metadata:reportcustomsqlqueries:timemodified'] = 'Time modified'; -$string['privacy_you'] = 'You'; +$string['privacy:metadata:reportcustomsqlqueries:usermodified'] = 'User modified'; $string['privacy_somebodyelse'] = 'Somebody else'; +$string['privacy_you'] = 'You'; $string['query_deleted'] = 'Query deleted'; $string['query_edited'] = 'Query edited'; $string['query_viewed'] = 'Query viewed'; @@ -183,14 +189,13 @@ $string['timemodified'] = 'Last modified: {$a}'; $string['typeofresult'] = 'Type of result'; $string['unknowndownloadfile'] = 'Unknown download file.'; -$string['usermodified'] = 'Modified by: {$a}'; -$string['usernotfound'] = 'User with id \'{$a}\' does not exist'; $string['userhasnothiscapability'] = 'User \'{$a->name}\' ({$a->userid}) has not got capability \'{$a->capability}\'. Please delete this user from the list or change the choice in \'{$a->whocanaccess}\'.'; $string['userinvalidinput'] = 'Invalid input, a comma-separated list of user names is required'; -$string['userswhocanviewsitereports'] = 'Users who can see system reports (moodle/site:viewreports)'; +$string['usermodified'] = 'Modified by: {$a}'; +$string['usernotfound'] = 'User with id \'{$a}\' does not exist'; $string['userswhocanconfig'] = 'Only administrators (moodle/site:config)'; +$string['userswhocanviewsitereports'] = 'Users who can see system reports (moodle/site:viewreports)'; $string['verifyqueryandupdate'] = 'Verify the Query SQL text and update the form'; $string['weeklyheader'] = 'Weekly'; $string['weeklyheader_help'] = 'These queries are automatically run on the first day of each week, to report on the previous week. These links let you view the results that has already been accumulated.'; $string['whocanaccess'] = 'Who can access this query'; -$string['privacy:metadata'] = 'The Ad-hoc database queries plugin does not store any personal data.'; diff --git a/locallib.php b/locallib.php index 7c9a6e6..a8167b7 100644 --- a/locallib.php +++ b/locallib.php @@ -29,6 +29,14 @@ define('REPORT_CUSTOMSQL_LIMIT_EXCEEDED_MARKER', '-- ROW LIMIT EXCEEDED --'); +/** + * Execute a custom SQL query. + * + * @param string $sql the SQL query. + * @param array $params the parameters to substitute into the query. + * @param int $limitnum the maximum number of rows to return. + * @return moodle_recordset a recordset. + */ function report_customsql_execute_query($sql, $params = null, $limitnum = null) { global $CFG, $DB; @@ -48,6 +56,13 @@ function report_customsql_execute_query($sql, $params = null, $limitnum = null) return $DB->get_recordset_sql($sql, $params, 0, $limitnum); } +/** + * Prepare the SQL query for execution. + * + * @param stdclass $report report record from customsql table. + * @param int $timenow unix timestamp - usually "now()" + * @return string the SQL query. + */ function report_customsql_prepare_sql($report, $timenow) { global $USER; $sql = $report->querysql; @@ -85,6 +100,7 @@ function report_customsql_get_query_placeholders_and_field_names(string $querysq /** * Return the type of form field to use for a placeholder, based on its name. + * * @param string $name the placeholder name. * @return string a formslib element type, for example 'text' or 'date_time_selector'. */ @@ -100,8 +116,9 @@ function report_customsql_get_element_type($name) { * Generate customsql csv file. * * @param stdclass $report report record from customsql table. - * @param int $timetimenow unix timestamp - usually "now()" + * @param int $timenow unix timestamp - usually "now()". * @param bool $returnheaderwhenempty if true, a CSV file with headers will always be generated, even if there are no results. + * @return int|null the timestamp of the CSV file, or null if there is no data. */ function report_customsql_generate_csv($report, $timenow, $returnheaderwhenempty = false) { global $DB; @@ -110,7 +127,7 @@ function report_customsql_generate_csv($report, $timenow, $returnheaderwhenempty $sql = report_customsql_prepare_sql($report, $timenow); $queryparams = !empty($report->queryparams) ? unserialize($report->queryparams) : []; - $querylimit = $report->querylimit ?? get_config('report_customsql', 'querylimitdefault'); + $querylimit = !empty($report->querylimit) ? $report->querylimit : get_config('report_customsql', 'querylimitdefault'); if ($returnheaderwhenempty) { // We want the export to always generate a CSV file so we modify the query slightly // to generate an extra "null" values row, so we can get the column names, @@ -120,7 +137,7 @@ function report_customsql_generate_csv($report, $timenow, $returnheaderwhenempty LEFT JOIN ($sql) as subq on true"; } // Query one extra row, so we can tell if we hit the limit. - $rs = report_customsql_execute_query($sql, $queryparams, $querylimit + 1); + $rs = report_customsql_execute_query($sql, $queryparams, !empty($querylimit) ? $querylimit + 1 : 0); $csvfilenames = []; $csvtimestamp = null; @@ -151,7 +168,7 @@ function report_customsql_generate_csv($report, $timenow, $returnheaderwhenempty } } if ($report->singlerow) { - array_unshift($data, \core_date::strftime('%Y-%m-%d', $timenow)); + array_unshift($data, date('Y-m-d', $timenow)); } report_customsql_write_csv_row($handle, $data); $count += 1; @@ -159,7 +176,7 @@ function report_customsql_generate_csv($report, $timenow, $returnheaderwhenempty $rs->close(); if (!empty($handle)) { - if ($count > $querylimit) { + if (!empty($querylimit) && $count > $querylimit) { report_customsql_write_csv_row($handle, [REPORT_CUSTOMSQL_LIMIT_EXCEEDED_MARKER]); } @@ -197,6 +214,8 @@ function report_customsql_generate_csv($report, $timenow, $returnheaderwhenempty } /** + * Validate a value as an integer. + * * @param mixed $value some value * @return bool whether $value is an integer, or a string that looks like an integer. */ @@ -204,6 +223,13 @@ function report_customsql_is_integer($value) { return (string) (int) $value === (string) $value; } +/** + * Create a csv file. + * + * @param stdclass $report report record from customsql table. + * @param int $timenow unix timestamp - usually "now()" + * @return string $csvfilename the CSV file to copy. + */ function report_customsql_csv_filename($report, $timenow) { if ($report->runable == 'manual') { return report_customsql_temp_cvs_name($report->id, $timenow); @@ -217,22 +243,42 @@ function report_customsql_csv_filename($report, $timenow) { } } +/** + * Create a temporary csv file. + * + * @param int $reportid the report id. + * @param int $timestamp the timestamp. + * @return array with two elements: the filename and the timestamp. + */ function report_customsql_temp_cvs_name($reportid, $timestamp) { global $CFG; $path = 'admin_report_customsql/temp/'.$reportid; make_upload_directory($path); - return [$CFG->dataroot.'/'.$path.'/'.\core_date::strftime('%Y%m%d-%H%M%S', $timestamp).'.csv', + return [$CFG->dataroot.'/'.$path.'/' . $timestamp . '.csv', $timestamp]; } +/** + * Create a scheduled csv file. + * + * @param int $reportid the report id. + * @param int $timestart the timestamp. + * @return array with two elements: the filename and the timestart. + */ function report_customsql_scheduled_cvs_name($reportid, $timestart) { global $CFG; $path = 'admin_report_customsql/'.$reportid; make_upload_directory($path); - return [$CFG->dataroot.'/'.$path.'/'.\core_date::strftime('%Y%m%d-%H%M%S', $timestart).'.csv', + return [$CFG->dataroot.'/'.$path.'/' . $timestart. '.csv', $timestart]; } +/** + * Create a accumulating csv file. + * + * @param int $reportid the report id. + * @return array + */ function report_customsql_accumulating_cvs_name($reportid) { global $CFG; $path = 'admin_report_customsql/'.$reportid; @@ -240,6 +286,12 @@ function report_customsql_accumulating_cvs_name($reportid) { return [$CFG->dataroot.'/'.$path.'/accumulate.csv', 0]; } +/** + * Get archive times for a report. + * + * @param stdclass $report report record from customsql table. + * @return array timestamps + */ function report_customsql_get_archive_times($report) { global $CFG; if ($report->runable == 'manual' || $report->singlerow) { @@ -257,10 +309,25 @@ function report_customsql_get_archive_times($report) { return $archivetimes; } +/** + * Substitutes the time tokens in the SQL query. + * + * @param string $sql the SQL query. + * @param int $start the start time. + * @param int $end the end time. + * @return string the SQL query with the time tokens substituted. + */ function report_customsql_substitute_time_tokens($sql, $start, $end) { return str_replace(['%%STARTTIME%%', '%%ENDTIME%%'], [$start, $end], $sql); } +/** + * Substitutes the user token in the SQL query. + * + * @param string $sql the SQL query. + * @param int $userid the user id. + * @return string the SQL query with the user token substituted. + */ function report_customsql_substitute_user_token($sql, $userid) { return str_replace('%%USERID%%', $userid, $sql); } @@ -300,6 +367,11 @@ function report_customsql_downloadurl($reportid, $params = []) { return $downloadurl; } +/** + * Supported capabilities. + * + * @return array + */ function report_customsql_capability_options() { return [ 'report/customsql:view' => get_string('anyonewhocanveiwthisreport', 'report_customsql'), @@ -308,6 +380,13 @@ function report_customsql_capability_options() { ]; } + +/** + * Get the list on run options. + * + * @param string $type the type of report (manual, daily, weekly or monthly). + * @return array + */ function report_customsql_runable_options($type = null) { if ($type === 'manual') { return ['manual' => get_string('manual', 'report_customsql')]; @@ -320,6 +399,11 @@ function report_customsql_runable_options($type = null) { ]; } +/** + * Get the list of time options. + * + * @return array + */ function report_customsql_daily_at_options() { $time = []; for ($h = 0; $h < 24; $h++) { @@ -329,33 +413,67 @@ function report_customsql_daily_at_options() { return $time; } +/** + * Get the list of email options. + * + * @return array + */ function report_customsql_email_options() { return ['emailnumberofrows' => get_string('emailnumberofrows', 'report_customsql'), 'emailresults' => get_string('emailresults', 'report_customsql'), ]; } +/** + * Get the list of bad words. + * + * @return array + */ function report_customsql_bad_words_list() { return ['ALTER', 'CREATE', 'DELETE', 'DROP', 'GRANT', 'INSERT', 'INTO', 'TRUNCATE', 'UPDATE']; } +/** + * Validate the query contains no bad words. + * + * @param string $string the query. + * @return bool + */ function report_customsql_contains_bad_word($string) { return preg_match('/\b('.implode('|', report_customsql_bad_words_list()).')\b/i', $string); } +/** + * Trigger a report_customsql\event\query_deleted event. + * + * @param int $id the id of the deleted query. + * @return void + */ function report_customsql_log_delete($id) { $event = \report_customsql\event\query_deleted::create( ['objectid' => $id, 'context' => context_system::instance()]); $event->trigger(); } +/** + * Trigger a report_customsql\event\query_edited event. + * + * @param int $id the id of the edit query. + * @return void + */ function report_customsql_log_edit($id) { $event = \report_customsql\event\query_edited::create( ['objectid' => $id, 'context' => context_system::instance()]); $event->trigger(); } +/** + * Trigger a report_customsql\event\query_viewed event. + * + * @param int $id the id of the view query. + * @return void + */ function report_customsql_log_view($id) { $event = \report_customsql\event\query_viewed::create( ['objectid' => $id, 'context' => context_system::instance()]); @@ -366,8 +484,8 @@ function report_customsql_log_view($id) { * Returns all reports for a given type sorted by report 'displayname'. * * @param int $categoryid - * @param string $type, type of report (manual, daily, weekly or monthly) - * @return stdClass[] relevant rows from report_customsql_queries. + * @param string $type type of report (manual, daily, weekly or monthly) + * @return array relevant rows from report_customsql_queries. */ function report_customsql_get_reports_for($categoryid, $type) { global $DB; @@ -380,8 +498,9 @@ function report_customsql_get_reports_for($categoryid, $type) { /** * Display a list of reports of one type in one category. * - * @param object $reports, the result of DB query - * @param string $type, type of report (manual, daily, weekly or monthly) + * @param array $reports the result of DB query + * @param string $type type of report (manual, daily, weekly or monthly) + * @return void */ function report_customsql_print_reports_for($reports, $type) { global $OUTPUT; @@ -487,6 +606,13 @@ function report_customsql_display_row($row, $linkcolumns) { return $rowdata; } +/** + * Time note for a report. + * + * @param stdClass $report report record from customsql table. + * @param string $tag the tag to use for the note. + * @return string the note. + */ function report_customsql_time_note($report, $tag) { if ($report->lastrun) { $a = new stdClass; @@ -501,7 +627,13 @@ function report_customsql_time_note($report, $tag) { return html_writer::tag($tag, $note, ['class' => 'admin_note']); } - +/** + * Generate pretty column names from a row of data. + * + * @param stdClass $row a row of data. + * @param string $querysql the query that generated the row. + * @return string[] the column names. + */ function report_customsql_pretify_column_names($row, $querysql) { $colnames = []; @@ -559,6 +691,13 @@ function report_customsql_read_csv_row($handle) { return fgetcsv($handle, 0, ',', '"', $disablestupidphpescaping); } +/** + * Start a CSV file. + * + * @param resource $handle the file pointer. + * @param stdClass $firstrow the first row of data. + * @param stdClass $report report record from customsql table. + */ function report_customsql_start_csv($handle, $firstrow, $report) { $colnames = report_customsql_pretify_column_names($firstrow, $report->querysql); if ($report->singlerow) { @@ -568,6 +707,8 @@ function report_customsql_start_csv($handle, $firstrow, $report) { } /** + * Daily time start. + * * @param int $timenow a timestamp. * @param int $at an hour, 0 to 23. * @return array with two elements: the timestamp for hour $at today (where today @@ -585,6 +726,12 @@ function report_customsql_get_daily_time_starts($timenow, $at) { ]; } +/** + * Time start of the week. + * + * @param int $timenow a timestamp. + * @return array with two elements: the timestamp for the start of the week + */ function report_customsql_get_week_starts($timenow) { $dateparts = getdate($timenow); @@ -603,6 +750,12 @@ function report_customsql_get_week_starts($timenow) { ]; } +/** + * Time start of the month. + * + * @param int $timenow a timestamp. + * @return array with two elements: the timestamp for the start of the month + */ function report_customsql_get_month_starts($timenow) { $dateparts = getdate($timenow); @@ -612,6 +765,13 @@ function report_customsql_get_month_starts($timenow) { ]; } +/** + * Get the start times for a report. + * + * @param stdClass $report report record from customsql table. + * @param int $timenow unix timestamp - usually "now()" + * @return array with two elements: the start time and the end time. + */ function report_customsql_get_starts($report, $timenow) { switch ($report->runable) { case 'daily': @@ -625,11 +785,17 @@ function report_customsql_get_starts($report, $timenow) { } } +/** + * Get old temp files. + * + * @param int $upto unix timestamp - usually "now()" + * @return int number of files deleted. + */ function report_customsql_delete_old_temp_files($upto) { global $CFG; $count = 0; - $comparison = \core_date::strftime('%Y%m%d-%H%M%S', $upto).'csv'; + $comparison = $upto . 'csv'; $files = glob($CFG->dataroot.'/admin_report_customsql/temp/*/*.csv'); if (empty($files)) { @@ -678,6 +844,12 @@ function report_customsql_validate_users($userids, $capability) { return null; } +/** + * Get message for email when there is no data. + * + * @param stdClass $report report settings from the database. + * @return stdClass the message object. + */ function report_customsql_get_message_no_data($report) { // Construct subject. $subject = report_customsql_email_subject(0, $report); @@ -696,6 +868,13 @@ function report_customsql_get_message_no_data($report) { return $message; } +/** + * Get message for email when there is data. + * + * @param stdClass $report report settings from the database. + * @param string $csvfilename the CSV file to copy. + * @return stdClass the message object. + */ function report_customsql_get_message($report, $csvfilename) { $handle = fopen($csvfilename, 'r'); $table = new html_table(); @@ -776,6 +955,12 @@ function report_customsql_email_subject(int $countrows, stdClass $report): strin } } +/** + * Email the report. + * + * @param stdClass $report report settings from the database. + * @param string $csvfilename the CSV file to copy. + */ function report_customsql_email_report($report, $csvfilename = null) { global $DB; @@ -801,6 +986,12 @@ function report_customsql_email_report($report, $csvfilename = null) { } } +/** + * Get the list of reports that are ready to run. + * + * @param int $timenow unix timestamp - usually "now()" + * @return array + */ function report_customsql_get_ready_to_run_daily_reports($timenow) { global $DB; $reports = $DB->get_records_select('report_customsql_queries', "runable = ?", ['daily'], 'id'); @@ -863,6 +1054,11 @@ function report_customsql_is_daily_report_ready($report, $timenow) { return false; } +/** + * Get the list of categories. + * + * @return array + */ function report_customsql_category_options() { global $DB; return $DB->get_records_menu('report_customsql_categories', null, 'name ASC', 'id, name'); diff --git a/manage.php b/manage.php index 3640da3..8d3ca2e 100644 --- a/manage.php +++ b/manage.php @@ -27,6 +27,7 @@ * @copyright 2013 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ + require_once(dirname(__FILE__) . '/../../config.php'); require_once(dirname(__FILE__) . '/locallib.php'); require_once($CFG->libdir . '/adminlib.php'); diff --git a/settings.php b/settings.php index 06e4664..9e80ae0 100644 --- a/settings.php +++ b/settings.php @@ -46,6 +46,10 @@ $settings->add(new admin_setting_configtext_with_maxlength('report_customsql/querylimitmaximum', get_string('querylimitmaximum', 'report_customsql'), get_string('querylimitmaximum_desc', 'report_customsql'), 5000, PARAM_INT, null, 10)); + + $settings->add(new admin_setting_configtext_with_maxlength('report_customsql/limittestrows', + get_string('limittestrows', 'report_customsql'), + get_string('limittestrows_desc', 'report_customsql'), 0, PARAM_INT, null, 2)); } $ADMIN->add('reports', new admin_externalpage('report_customsql', diff --git a/tests/external/external_create_query_test.php b/tests/external/external_create_query_test.php new file mode 100644 index 0000000..ed9f01b --- /dev/null +++ b/tests/external/external_create_query_test.php @@ -0,0 +1,81 @@ +. + +namespace report_customsql\external; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); + + +/** + * Tests for the create_query web service. + * + * @package report_customsql + * @category external + * @author Oscar Nadjar + * @copyright 2023 Moodle Us + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \report_customsql\external\create_query + * @runTestsInSeparateProcesses + */ +final class external_create_query_test extends \externallib_advanced_testcase { + + protected function setUp(): void { + parent::setUp(); + $this->resetAfterTest(); + $this->setAdminUser(); + } + + public function test_create_query(): void { + + global $DB; + + $displayname = 'test'; + $description = 'test'; + $querysql = 'SELECT * FROM {user}'; + $queryparams = ''; + $querylimit = 5000; + $capability = 'moodle/site:config'; + $runable = 'manual'; + $at = ''; + $emailto = 'test@mail.com'; + $emailwhat = 'Test email'; + $categoryid = 1; + $customdir = ''; + + $result = create_query::execute( + $displayname, $description, $querysql, $queryparams, $querylimit, + $capability, $runable, $at, $emailto, $emailwhat, $categoryid, $customdir); + $result = \external_api::clean_returnvalue(create_query::execute_returns(), $result); + + $query = $DB->get_record('report_customsql_queries', []); + $this->assertEquals($query->displayname, $displayname); + $this->assertEquals($query->description, $description); + $this->assertEquals($query->querysql, $querysql); + $this->assertEquals($query->queryparams, $queryparams); + $this->assertEquals($query->querylimit, $querylimit); + $this->assertEquals($query->capability, $capability); + $this->assertEquals($query->runable, $runable); + $this->assertEquals($query->at, $at); + $this->assertEquals($query->emailto, $emailto); + $this->assertEquals($query->emailwhat, $emailwhat); + $this->assertEquals($query->categoryid, $categoryid); + $this->assertEquals($query->customdir, $customdir); + } +} diff --git a/tests/external/external_delete_query_test.php b/tests/external/external_delete_query_test.php new file mode 100644 index 0000000..9828115 --- /dev/null +++ b/tests/external/external_delete_query_test.php @@ -0,0 +1,75 @@ +. + +namespace report_customsql\external; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); + + +/** + * Tests for the delete_query web service. + * + * @package report_customsql + * @category external + * @author Oscar Nadjar + * @copyright 2023 Moodle Us + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \report_customsql\external\delete_query + * @runTestsInSeparateProcesses + */ +final class external_delete_query_test extends \externallib_advanced_testcase { + + protected function setUp(): void { + parent::setUp(); + $this->resetAfterTest(); + $this->setAdminUser(); + } + + public function test_delete_query(): void { + + global $DB; + + $displayname = 'test'; + $description = 'test'; + $querysql = 'SELECT * FROM {user}'; + $queryparams = ''; + $querylimit = 5000; + $capability = 'moodle/site:config'; + $runable = 'manual'; + $at = ''; + $emailto = 'test@mail.com'; + $emailwhat = 'Test email'; + $categoryid = 1; + $customdir = ''; + + $result = create_query::execute( + $displayname, $description, $querysql, $queryparams, $querylimit, + $capability, $runable, $at, $emailto, $emailwhat, $categoryid, $customdir); + $result = \external_api::clean_returnvalue(create_query::execute_returns(), $result); + + $query = $DB->get_record('report_customsql_queries', []); + + $result = delete_query::execute($query->id); + $result = \external_api::clean_returnvalue(delete_query::execute_returns(), $result); + + $query = $DB->get_record('report_customsql_queries', ['id' => $query->id]); + $this->assertFalse($query); + } +} diff --git a/tests/external/external_get_query_results_test.php b/tests/external/external_get_query_results_test.php new file mode 100644 index 0000000..25b7c8b --- /dev/null +++ b/tests/external/external_get_query_results_test.php @@ -0,0 +1,81 @@ +. + +namespace report_customsql\external; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); + + +/** + * Tests for the get_query_results web service. + * + * @package report_customsql + * @category external + * @author Oscar Nadjar + * @copyright 2023 Moodle Us + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \report_customsql\external\get_query_results + * @runTestsInSeparateProcesses + */ +final class external_get_query_results_test extends \externallib_advanced_testcase { + + protected function setUp(): void { + parent::setUp(); + $this->resetAfterTest(); + $this->setAdminUser(); + } + + public function test_get_query_results(): void { + + global $DB; + + $displayname = 'test'; + $description = 'test'; + $querysql = 'SELECT * FROM {user}'; + $queryparams = ''; + $querylimit = 5000; + $capability = 'moodle/site:config'; + $runable = 'manual'; + $at = ''; + $emailto = 'test@mail.com'; + $emailwhat = 'Test email'; + $categoryid = 1; + $customdir = ''; + + $result = create_query::execute( + $displayname, $description, $querysql, $queryparams, $querylimit, + $capability, $runable, $at, $emailto, $emailwhat, $categoryid, $customdir); + $result = \external_api::clean_returnvalue(create_query::execute_returns(), $result); + + $report = $DB->get_record('report_customsql_queries', ['id' => $result['queryid']]); + $csvtimestamp = report_customsql_generate_csv($report, time()); + $result = get_query_results::execute($report->id, 'csv'); + $result = \external_api::clean_returnvalue(get_query_results::execute_returns(), $result); + $result = reset($result['results']); + + $date = date('Y-m-d H:i:s', $csvtimestamp); + $this->assertEquals($date, $result['date']); + + $url = new \moodle_url('/webservice/pluginfile.php/' . + \context_system::instance()->id . '/report_customsql/download/' . $report->id . '/'); + $donwloadurl = new \moodle_url($url, ['dataformat' => 'csv', 'timestamp' => $csvtimestamp]); + $this->assertEquals($donwloadurl->out(false), $result['downloadurl']); + } +} diff --git a/tests/external/external_get_users_test.php b/tests/external/external_get_users_test.php index 5a1e1af..9968ac8 100644 --- a/tests/external/external_get_users_test.php +++ b/tests/external/external_get_users_test.php @@ -14,7 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . - namespace report_customsql\external; defined('MOODLE_INTERNAL') || die(); @@ -34,8 +33,12 @@ * @covers \report_customsql\external\get_users * @runTestsInSeparateProcesses */ -class external_get_users_test extends \externallib_advanced_testcase { +final class external_get_users_test extends \externallib_advanced_testcase { + /** + * Set up the test case. + * @return array + */ protected function setup_users(): array { global $DB, $USER; diff --git a/tests/external/external_list_queries_test.php b/tests/external/external_list_queries_test.php new file mode 100644 index 0000000..334253a --- /dev/null +++ b/tests/external/external_list_queries_test.php @@ -0,0 +1,88 @@ +. + +namespace report_customsql\external; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); + + +/** + * Tests for the create_query web service. + * + * @package report_customsql + * @category external + * @author Oscar Nadjar + * @copyright 2023 Moodle Us + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \report_customsql\external\list_queries + * @runTestsInSeparateProcesses + */ +final class external_list_queries_test extends \externallib_advanced_testcase { + + protected function setUp(): void { + parent::setUp(); + $this->resetAfterTest(); + $this->setAdminUser(); + } + + public function test_list_queries(): void { + + global $DB; + + $displayname = 'test'; + $description = 'test'; + $querysql = 'SELECT * FROM {user}'; + $queryparams = ''; + $querylimit = 5000; + $capability = 'moodle/site:config'; + $runable = 'manual'; + $at = ''; + $emailto = 'test@mail.com'; + $emailwhat = 'Test email'; + $categoryid = 1; + $customdir = ''; + + $result = create_query::execute( + $displayname, $description, $querysql, $queryparams, $querylimit, + $capability, $runable, $at, $emailto, $emailwhat, $categoryid, $customdir); + $result = \external_api::clean_returnvalue(create_query::execute_returns(), $result); + + $displayname = 'test2'; + + $result = create_query::execute( + $displayname, $description, $querysql, $queryparams, $querylimit, + $capability, $runable, $at, $emailto, $emailwhat, $categoryid, $customdir); + $result = \external_api::clean_returnvalue(create_query::execute_returns(), $result); + + $querylistpage1 = list_queries::execute(1, 1); + $querylistpage1 = \external_api::clean_returnvalue(list_queries::execute_returns(), $querylistpage1); + + $queries = $querylistpage1['queries']; + $this->assertEquals(1, count($queries)); + $this->assertEquals('test', $queries[0]['displayname']); + + $querylistpage2 = list_queries::execute(2, 1); + $querylistpage2 = \external_api::clean_returnvalue(list_queries::execute_returns(), $querylistpage2); + + $queries = $querylistpage2['queries']; + $this->assertEquals(1, count($queries)); + $this->assertEquals('test2', $queries[0]['displayname']); + } +} diff --git a/tests/external/external_query_details_test.php b/tests/external/external_query_details_test.php new file mode 100644 index 0000000..ced1443 --- /dev/null +++ b/tests/external/external_query_details_test.php @@ -0,0 +1,84 @@ +. + +namespace report_customsql\external; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); + + +/** + * Tests for the query_details web service. + * + * @package report_customsql + * @category external + * @author Oscar Nadjar + * @copyright 2023 Moodle Us + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \report_customsql\external\query_details + * @runTestsInSeparateProcesses + */ +final class external_query_details_test extends \externallib_advanced_testcase { + + protected function setUp(): void { + parent::setUp(); + $this->resetAfterTest(); + $this->setAdminUser(); + } + + public function test_query_details(): void { + + global $DB; + + $displayname = 'test'; + $description = 'test'; + $querysql = 'SELECT * FROM {user}'; + $queryparams = ''; + $querylimit = 5000; + $capability = 'moodle/site:config'; + $runable = 'manual'; + $at = ''; + $emailto = 'test@mail.com'; + $emailwhat = 'Test email'; + $categoryid = 1; + $customdir = ''; + + $result = create_query::execute( + $displayname, $description, $querysql, $queryparams, $querylimit, + $capability, $runable, $at, $emailto, $emailwhat, $categoryid, $customdir); + $result = \external_api::clean_returnvalue(create_query::execute_returns(), $result); + + $querydetails = query_details::execute($result['queryid']); + $querydetails = \external_api::clean_returnvalue(query_details::execute_returns(), $querydetails); + $querydetails = $querydetails['query']; + + $this->assertEquals($querydetails['displayname'], $displayname); + $this->assertEquals($querydetails['description'], $description); + $this->assertEquals($querydetails['querysql'], $querysql); + $this->assertEquals($querydetails['queryparams'], $queryparams); + $this->assertEquals($querydetails['querylimit'], $querylimit); + $this->assertEquals($querydetails['capability'], $capability); + $this->assertEquals($querydetails['runable'], $runable); + $this->assertEquals($querydetails['at'], $at); + $this->assertEquals($querydetails['emailto'], $emailto); + $this->assertEquals($querydetails['emailwhat'], $emailwhat); + $this->assertEquals($querydetails['categoryid'], $categoryid); + $this->assertEquals($querydetails['customdir'], $customdir); + } +} diff --git a/tests/external/external_update_query_test.php b/tests/external/external_update_query_test.php new file mode 100644 index 0000000..77e3391 --- /dev/null +++ b/tests/external/external_update_query_test.php @@ -0,0 +1,101 @@ +. + +namespace report_customsql\external; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); + + +/** + * Tests for the upddate_query web service. + * + * @package report_customsql + * @category external + * @author Oscar Nadjar + * @copyright 2023 Moodle Us + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \report_customsql\external\upddate_query + * @runTestsInSeparateProcesses + */ +final class external_update_query_test extends \externallib_advanced_testcase { + + protected function setUp(): void { + parent::setUp(); + $this->resetAfterTest(); + $this->setAdminUser(); + } + + public function test_upddate_query(): void { + + global $DB; + + $displayname = 'test'; + $description = 'test'; + $querysql = 'SELECT * FROM {user}'; + $queryparams = ''; + $querylimit = 5000; + $capability = 'moodle/site:config'; + $runable = 'manual'; + $at = ''; + $emailto = 'test@mail.com'; + $emailwhat = 'Test email'; + $categoryid = 1; + $customdir = ''; + + $result = create_query::execute( + $displayname, $description, $querysql, $queryparams, $querylimit, + $capability, $runable, $at, $emailto, $emailwhat, $categoryid, $customdir); + $result = \external_api::clean_returnvalue(create_query::execute_returns(), $result); + + $query = $DB->get_record('report_customsql_queries', []); + + $displayname = 'testupdate'; + $description = 'testupdate'; + $querysql = 'SELECT id FROM {user}'; + $queryparams = ''; + $querylimit = 6000; + $capability = 'moodle/site:config'; + $runable = 'manual'; + $at = ''; + $emailto = 'testupdate@mail.com'; + $emailwhat = 'Test email update'; + $categoryid = 1; + $customdir = '/updatedir'; + + $result = update_query::execute( $query->id, + $displayname, $description, $querysql, $queryparams, $querylimit, + $capability, $runable, $at, $emailto, $emailwhat, $categoryid, $customdir); + $result = \external_api::clean_returnvalue(update_query::execute_returns(), $result); + + $updatedquery = $DB->get_record('report_customsql_queries', []); + $this->assertEquals($updatedquery->displayname, $displayname); + $this->assertEquals($updatedquery->description, $description); + $this->assertEquals($updatedquery->querysql, $querysql); + $this->assertEquals($updatedquery->queryparams, $queryparams); + $this->assertEquals($updatedquery->querylimit, $querylimit); + $this->assertEquals($updatedquery->capability, $capability); + $this->assertEquals($updatedquery->runable, $runable); + $this->assertEquals($updatedquery->at, $at); + $this->assertEquals($updatedquery->emailto, $emailto); + $this->assertEquals($updatedquery->emailwhat, $emailwhat); + $this->assertEquals($updatedquery->categoryid, $categoryid); + $this->assertEquals($updatedquery->customdir, $customdir); + } +} diff --git a/tests/local/category_test.php b/tests/local/category_test.php index 7bb72bb..253320c 100644 --- a/tests/local/category_test.php +++ b/tests/local/category_test.php @@ -29,7 +29,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \report_customsql\local\category */ -class category_test extends \advanced_testcase { +final class category_test extends \advanced_testcase { /** * Test create category. */ diff --git a/tests/local/query_test.php b/tests/local/query_test.php index 1f1fb95..8c85864 100644 --- a/tests/local/query_test.php +++ b/tests/local/query_test.php @@ -29,7 +29,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \report_customsql\local\query */ -class query_test extends \advanced_testcase { +final class query_test extends \advanced_testcase { /** * Test create query. */ diff --git a/tests/privacy_test.php b/tests/privacy_test.php index 55644dc..2562119 100644 --- a/tests/privacy_test.php +++ b/tests/privacy_test.php @@ -27,7 +27,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \report_customsql\privacy\provider */ -class privacy_test extends \core_privacy\tests\provider_testcase { +final class privacy_test extends \core_privacy\tests\provider_testcase { /** @var \stdClass test user. */ protected $user1; @@ -37,6 +37,7 @@ class privacy_test extends \core_privacy\tests\provider_testcase { protected $user3; public function setUp(): void { + parent::setUp(); $this->resetAfterTest(); $this->setAdminUser(); diff --git a/tests/report_test.php b/tests/report_test.php index d52efcf..bbd9e0e 100644 --- a/tests/report_test.php +++ b/tests/report_test.php @@ -28,7 +28,7 @@ * @copyright 2009 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class report_test extends \advanced_testcase { +final class report_test extends \advanced_testcase { /** * Data provider for test_get_week_starts @@ -97,7 +97,11 @@ public function test_get_week_starts_use_calendar_default( $this->assertEquals($expected, report_customsql_get_week_starts(strtotime($datestr))); } - /** @covers ::report_customsql_get_month_starts */ + + /** + * Test plugin get_month_starts method. + * @covers ::report_customsql_get_month_starts + */ public function test_get_month_starts_test(): void { $this->assertEquals([ strtotime('00:00 1 November 2009'), strtotime('00:00 1 October 2009')], @@ -112,7 +116,10 @@ public function test_get_month_starts_test(): void { report_customsql_get_month_starts(strtotime('23:59 29 November 2009'))); } - /** @covers ::report_customsql_get_element_type */ + /** + * Test element type detection. + * @covers ::report_customsql_get_element_type + */ public function test_report_customsql_get_element_type(): void { $this->assertEquals('date_time_selector', report_customsql_get_element_type('start_date')); $this->assertEquals('date_time_selector', report_customsql_get_element_type('startdate')); @@ -124,14 +131,20 @@ public function test_report_customsql_get_element_type(): void { $this->assertEquals('text', report_customsql_get_element_type('mandated')); } - /** @covers ::report_customsql_substitute_user_token */ + /** + * Test token substitution. + * @covers ::report_customsql_substitute_user_token + */ public function test_report_customsql_substitute_user_token(): void { $this->assertEquals('SELECT COUNT(*) FROM oh_quiz_attempts WHERE user = 123', report_customsql_substitute_user_token('SELECT COUNT(*) FROM oh_quiz_attempts '. 'WHERE user = %%USERID%%', 123)); } - /** @covers ::report_customsql_capability_options */ + /** + * Test capability options. + * @covers ::report_customsql_capability_options + */ public function test_report_customsql_capability_options(): void { $capoptions = [ 'report/customsql:view' => get_string('anyonewhocanveiwthisreport', 'report_customsql'), @@ -142,7 +155,10 @@ public function test_report_customsql_capability_options(): void { } - /** @covers ::report_customsql_runable_options */ + /** + * Test runable options. + * @covers ::report_customsql_runable_options + */ public function test_report_customsql_runable_options(): void { $options = [ 'manual' => get_string('manual', 'report_customsql'), @@ -154,7 +170,10 @@ public function test_report_customsql_runable_options(): void { $this->assertEquals($options, report_customsql_runable_options()); } - /** @covers ::report_customsql_daily_at_options */ + /** + * Test daily run options. + * @covers ::report_customsql_daily_at_options + */ public function test_report_customsql_daily_at_options(): void { $time = []; for ($h = 0; $h < 24; $h++) { @@ -164,7 +183,10 @@ public function test_report_customsql_daily_at_options(): void { $this->assertEquals($time, report_customsql_daily_at_options()); } - /** @covers ::report_customsql_email_options */ + /** + * Test email options. + * @covers ::report_customsql_email_options + */ public function test_report_customsql_email_options(): void { $options = [ 'emailnumberofrows' => get_string('emailnumberofrows', 'report_customsql'), @@ -173,19 +195,28 @@ public function test_report_customsql_email_options(): void { $this->assertEquals($options, report_customsql_email_options()); } - /** @covers ::report_customsql_bad_words_list */ + /** + * Test bad words list. + * @covers ::report_customsql_bad_words_list + */ public function test_report_customsql_bad_words_list(): void { $options = ['ALTER', 'CREATE', 'DELETE', 'DROP', 'GRANT', 'INSERT', 'INTO', 'TRUNCATE', 'UPDATE']; $this->assertEquals($options, report_customsql_bad_words_list()); } - /** @covers ::report_customsql_bad_words_list */ + /** + * Test bad words. + * @covers ::report_customsql_contains_bad_word + * */ public function test_report_customsql_contains_bad_word(): void { $string = 'DELETE * FROM prefix_user u WHERE u.id > 0'; $this->assertEquals(1, report_customsql_contains_bad_word($string)); } - /** @covers ::report_customsql_get_daily_time_starts */ + /** + * Test daily reports. + * @covers ::report_customsql_get_daily_time_starts + */ public function test_report_customsql_get_ready_to_run_daily_reports(): void { global $DB; $this->resetAfterTest(true); @@ -259,7 +290,10 @@ public function test_report_customsql_get_ready_to_run_daily_reports(): void { $this->assertTrue(report_customsql_is_daily_report_ready($report, $timenow)); } - /** @covers ::report_customsql_is_integer */ + /** + * Test integer detection. + * @covers ::report_customsql_is_integer + */ public function test_report_customsql_is_integer(): void { $this->assertTrue(report_customsql_is_integer(1)); $this->assertTrue(report_customsql_is_integer('1')); @@ -267,7 +301,10 @@ public function test_report_customsql_is_integer(): void { $this->assertFalse(report_customsql_is_integer('2013-10-07')); } - /** @covers ::report_customsql_get_table_headers */ + /** + * Test table headers. + * @covers ::report_customsql_get_table_headers + */ public function test_report_customsql_get_table_headers(): void { $rawheaders = [ 'String date', @@ -294,7 +331,10 @@ public function test_report_customsql_get_table_headers(): void { $this->assertEquals([3 => 4, 4 => -1, 5 => 7, 7 => -1], $linkcolumns); } - /** @covers ::report_customsql_pretify_column_names */ + /** + * Test column names. + * @covers ::report_customsql_pretify_column_names + */ public function test_report_customsql_pretify_column_names(): void { $row = new \stdClass(); $row->column = 1; @@ -305,7 +345,10 @@ public function test_report_customsql_pretify_column_names(): void { report_customsql_pretify_column_names($row, $query)); } - /** @covers ::report_customsql_pretify_column_names */ + /** + * Test column multi-line names. + * @covers ::report_customsql_pretify_column_names + */ public function test_report_customsql_pretify_column_names_multi_line(): void { $row = new \stdClass(); $row->column = 1; @@ -320,7 +363,10 @@ public function test_report_customsql_pretify_column_names_multi_line(): void { report_customsql_pretify_column_names($row, $query)); } - /** @covers ::report_customsql_pretify_column_names */ + /** + * Test pretty column names. + * @covers ::report_customsql_pretify_column_names + */ public function test_report_customsql_pretify_column_names_same_name_diff_capitialisation(): void { $row = new \stdClass(); $row->course = 'B747-19B'; @@ -331,7 +377,10 @@ public function test_report_customsql_pretify_column_names_same_name_diff_capiti } - /** @covers ::report_customsql_pretify_column_names */ + /** + * Test pretty column names. + * @covers ::report_customsql_pretify_column_names + */ public function test_report_customsql_pretify_column_names_issue(): void { $row = new \stdClass(); $row->website = 'B747-19B'; @@ -362,7 +411,10 @@ public function test_report_customsql_pretify_column_names_issue(): void { } - /** @covers ::report_customsql_display_row */ + /** + * Test row display. + * @covers ::report_customsql_display_row + */ public function test_report_customsql_display_row(): void { $rawdata = [ 'Not a date', @@ -495,7 +547,10 @@ public function test_report_custom_sql_download_report_url(): void { $this->assertEquals($expected, $url->out(false)); } - /** @covers ::report_customsql_write_csv_row */ + /** + * Test writing a CSV row. + * @covers ::report_customsql_write_csv_row + */ public function test_report_customsql_write_csv_row(): void { global $CFG; $this->resetAfterTest(); diff --git a/version.php b/version.php index d2a47c9..23214fc 100644 --- a/version.php +++ b/version.php @@ -24,8 +24,8 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2023121300; -$plugin->requires = 2022112800; +$plugin->version = 2024010408; +$plugin->requires = 2018051700; $plugin->component = 'report_customsql'; $plugin->maturity = MATURITY_STABLE; $plugin->release = '4.3 for Moodle 4.1+'; diff --git a/view.php b/view.php index 2c23af6..c85fb0d 100644 --- a/view.php +++ b/view.php @@ -207,7 +207,7 @@ if ($rowlimitexceeded) { echo html_writer::tag('p', get_string('recordlimitreached', 'report_customsql', - $report->querylimit ?? get_config('report_customsql', 'querylimitdefault')), + !empty($report->querylimit) ? $report->querylimit : get_config('report_customsql', 'querylimitdefault')), ['class' => 'admin_note']); } else { echo html_writer::tag('p', get_string('recordcount', 'report_customsql', $count), diff --git a/view_form.php b/view_form.php index d4d9c32..ef80341 100644 --- a/view_form.php +++ b/view_form.php @@ -34,6 +34,10 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class report_customsql_view_form extends moodleform { + + /** + * Define the form. + */ public function definition() { $mform = $this->_form;