Merge branch 'MDL-70794' of git://github.com/paulholden/moodle

This commit is contained in:
abgreeve
2021-08-02 12:39:32 +08:00
97 changed files with 10628 additions and 3 deletions
+67
View File
@@ -0,0 +1,67 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Strings for component 'reportbuilder', language 'en'
*
* @package core_reportbuilder
* @copyright 2020 Sara Arjona <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['actions'] = 'Actions';
$string['apply'] = 'Apply';
$string['coursefullnamewithlink'] = 'Course full name with link';
$string['courseidnumberewithlink'] = 'Course ID number with link';
$string['courseshortnamewithlink'] = 'Course short name with link';
$string['customfieldcolumn'] = '{$a}';
$string['entitycourse'] = 'Course';
$string['entityuser'] = 'User';
$string['errorreportaccess'] = 'You can not view this report';
$string['errorsourceinvalid'] = 'Could not find valid report source';
$string['errorsourceunavailable'] = 'Report source is not available';
$string['filtercontains'] = 'Contains';
$string['filterdatefrom'] = 'Date from';
$string['filterdateto'] = 'Date to';
$string['filterdoesnotcontain'] = 'Does not contain';
$string['filterendswith'] = 'Ends with';
$string['filterequalorgreaterthan'] = 'Greater than or equal';
$string['filterequalorlessthan'] = 'Less than or equal';
$string['filterfieldoperator'] = '{$a} operator';
$string['filterfieldvalue'] = '{$a} value';
$string['filtergreaterthan'] = 'Greater than';
$string['filterinvalid'] = 'Invalid filter';
$string['filterisanyvalue'] = 'Is any value';
$string['filterisempty'] = 'Is empty';
$string['filterisequalto'] = 'Is equal to';
$string['filterisnotempty'] = 'Is not empty';
$string['filterisnotequalto'] = 'Is not equal to';
$string['filterlessthan'] = 'Less than';
$string['filterrange'] = 'Range';
$string['filtersapplied'] = 'Filters applied';
$string['filtersreset'] = 'Filters reset';
$string['filterstartswith'] = 'Starts with';
$string['privacy:metadata:preference:reportfilter'] = 'Stored report filter values';
$string['privacy:metadata:report'] = 'Report definitions';
$string['privacy:metadata:report:name'] = 'The name of the report';
$string['privacy:metadata:report:usercreated'] = 'The ID of the user who created the report';
$string['privacy:metadata:report:usermodified'] = 'The ID of the user who last modified the report';
$string['resetall'] = 'Reset all';
$string['selectcourses'] = 'Select courses';
$string['userfullnamewithlink'] = 'Full name with link';
$string['userfullnamewithpicture'] = 'Full name with picture';
$string['userfullnamewithpicturelink'] = 'Full name with picture and link';
$string['userpicture'] = 'User picture';
+1
View File
@@ -104,6 +104,7 @@
"privacy": "privacy",
"question": "question",
"rating": "rating",
"reportbuilder": "reportbuilder",
"repository": "repository",
"rss": "rss",
"role": "admin\/roles",
+22 -1
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="lib/db" VERSION="20210422" COMMENT="XMLDB file for core Moodle tables"
<XMLDB PATH="lib/db" VERSION="20210728" COMMENT="XMLDB file for core Moodle tables"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
>
@@ -4376,5 +4376,26 @@
<INDEX NAME="userid-issuerid-scopehash" UNIQUE="true" FIELDS="userid, issuerid, scopehash"/>
</INDEXES>
</TABLE>
<TABLE NAME="reportbuilder_report" COMMENT="Table to represent a report">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="source" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="type" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="contextid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="component" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="area" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="itemid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="usercreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="usermodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="usercreated" TYPE="foreign" FIELDS="usercreated" REFTABLE="user" REFFIELDS="id"/>
<KEY NAME="usermodified" TYPE="foreign" FIELDS="usermodified" REFTABLE="user" REFFIELDS="id"/>
<KEY NAME="contextid" TYPE="foreign" FIELDS="contextid" REFTABLE="context" REFFIELDS="id"/>
</KEYS>
</TABLE>
</TABLES>
</XMLDB>
+6
View File
@@ -2773,6 +2773,12 @@ $functions = array(
'type' => 'read',
'ajax' => true,
],
'core_reportbuilder_filters_reset' => [
'classname' => 'core_reportbuilder\external\filters\reset',
'description' => 'Reset filters for given report',
'type' => 'write',
'ajax' => true,
],
);
$services = array(
+32
View File
@@ -2675,5 +2675,37 @@ function xmldb_main_upgrade($oldversion) {
upgrade_main_savepoint(true, 2021060900.00);
}
if ($oldversion < 2021072800.01) {
// Define table reportbuilder_report to be created.
$table = new xmldb_table('reportbuilder_report');
// Adding fields to table reportbuilder_report.
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
$table->add_field('source', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null);
$table->add_field('type', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0');
$table->add_field('contextid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
$table->add_field('component', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null);
$table->add_field('area', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null);
$table->add_field('itemid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
$table->add_field('usercreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
$table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
$table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
$table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
// Adding keys to table reportbuilder_report.
$table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
$table->add_key('usercreated', XMLDB_KEY_FOREIGN, ['usercreated'], 'user', ['id']);
$table->add_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
$table->add_key('contextid', XMLDB_KEY_FOREIGN, ['contextid'], 'context', ['id']);
// Conditionally launch create table for reportbuilder_report.
if (!$dbman->table_exists($table)) {
$dbman->create_table($table);
}
// Main savepoint reached.
upgrade_main_savepoint(true, 2021072800.01);
}
return true;
}
+1 -1
View File
@@ -36,7 +36,7 @@ class component_test extends advanced_testcase {
* this is defined here to annoy devs that try to add more without any thinking,
* always verify that it does not collide with any existing add-on modules and subplugins!!!
*/
const SUBSYSTEMCOUNT = 73;
const SUBSYSTEMCOUNT = 74;
public function setUp(): void {
$psr0namespaces = new ReflectionProperty('core_component', 'psr0namespaces');
+3
View File
@@ -202,6 +202,9 @@
<testsuite name="core_contentbank_testsuite">
<directory suffix="_test.php">contentbank/tests</directory>
</testsuite>
<testsuite name="core_reportbuilder_testsuite">
<directory suffix="_test.php">reportbuilder/tests</directory>
</testsuite>
<!--Plugin suites: use admin/tool/phpunit/cli/util.php to build phpunit.xml from
phpunit.xml.dist with up-to-date list of plugins in current install-->
+2
View File
@@ -0,0 +1,2 @@
function _typeof(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){_typeof=function(a){return typeof a}}else{_typeof=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return _typeof(a)}define ("core_reportbuilder/filters",["exports","core/event_dispatcher","core/notification","core/pending","core/str","core/toast","core_form/dynamicform","core_reportbuilder/local/events","core_reportbuilder/local/selectors","core_reportbuilder/local/repository/filters"],function(a,b,c,d,e,f,g,h,i,j){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;c=m(c);d=m(d);g=m(g);h=l(h);i=l(i);function k(){if("function"!=typeof WeakMap)return null;var a=new WeakMap;k=function(){return a};return a}function l(a){if(a&&a.__esModule){return a}if(null===a||"object"!==_typeof(a)&&"function"!=typeof a){return{default:a}}var b=k();if(b&&b.has(a)){return b.get(a)}var c={},d=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var e in a){if(Object.prototype.hasOwnProperty.call(a,e)){var f=d?Object.getOwnPropertyDescriptor(a,e):null;if(f&&(f.get||f.set)){Object.defineProperty(c,e,f)}else{c[e]=a[e]}}}c.default=a;if(b){b.set(a,c)}return c}function m(a){return a&&a.__esModule?a:{default:a}}var n=function(a){var k=document.querySelector(i.forSystemReport(a)),l=k.querySelector(i.regions.filtersForm),m=new g.default(l,"\\core_reportbuilder\\form\\filter");m.addEventListener(m.events.FORM_SUBMITTED,function(a){a.preventDefault();(0,b.dispatchEvent)(h.tableReload,{},k);(0,e.get_string)("filtersapplied","core_reportbuilder").then(f.add).catch(c.default.exception)});m.addEventListener(m.events.NOSUBMIT_BUTTON_PRESSED,function(b){b.preventDefault();var g=new d.default("core_reportbuilder/filters:reset");(0,j.reset)(a).then(function(){return(0,e.get_string)("filtersreset","core_reportbuilder")}).then(f.add).then(function(){g.resolve();window.location.reload()}).catch(c.default.exception)});document.querySelector("#region-main").style.overflowX="visible"};a.init=n});
//# sourceMappingURL=filters.min.js.map
@@ -0,0 +1 @@
{"version":3,"sources":["../src/filters.js"],"names":["init","reportId","reportElement","document","querySelector","reportSelectors","forSystemReport","filterFormContainer","regions","filtersForm","filterForm","DynamicForm","addEventListener","events","FORM_SUBMITTED","event","preventDefault","reportEvents","tableReload","then","addToast","catch","Notification","exception","NOSUBMIT_BUTTON_PRESSED","pendingPromise","Pending","resolve","window","location","reload","style","overflowX"],"mappings":"8pBAyBA,OACA,OAGA,OACA,OACA,O,ylBAQO,GAAMA,CAAAA,CAAI,CAAG,SAAAC,CAAQ,CAAI,IACtBC,CAAAA,CAAa,CAAGC,QAAQ,CAACC,aAAT,CAAuBC,CAAe,CAACC,eAAhB,CAAgCL,CAAhC,CAAvB,CADM,CAEtBM,CAAmB,CAAGL,CAAa,CAACE,aAAd,CAA4BC,CAAe,CAACG,OAAhB,CAAwBC,WAApD,CAFA,CAGtBC,CAAU,CAAG,GAAIC,UAAJ,CAAgBJ,CAAhB,CAAqC,oCAArC,CAHS,CAM5BG,CAAU,CAACE,gBAAX,CAA4BF,CAAU,CAACG,MAAX,CAAkBC,cAA9C,CAA8D,SAAAC,CAAK,CAAI,CACnEA,CAAK,CAACC,cAAN,GAGA,oBAAcC,CAAY,CAACC,WAA3B,CAAwC,EAAxC,CAA4ChB,CAA5C,EAEA,iBAAU,gBAAV,CAA4B,oBAA5B,EACKiB,IADL,CACUC,KADV,EAEKC,KAFL,CAEWC,UAAaC,SAFxB,CAGH,CATD,EAYAb,CAAU,CAACE,gBAAX,CAA4BF,CAAU,CAACG,MAAX,CAAkBW,uBAA9C,CAAuE,SAAAT,CAAK,CAAI,CAC5EA,CAAK,CAACC,cAAN,GAEA,GAAMS,CAAAA,CAAc,CAAG,GAAIC,UAAJ,CAAY,kCAAZ,CAAvB,CAEA,YAAazB,CAAb,EACKkB,IADL,CACU,iBAAM,iBAAU,cAAV,CAA0B,oBAA1B,CAAN,CADV,EAEKA,IAFL,CAEUC,KAFV,EAGKD,IAHL,CAGU,UAAM,CACRM,CAAc,CAACE,OAAf,GACAC,MAAM,CAACC,QAAP,CAAgBC,MAAhB,EAEH,CAPL,EAQKT,KARL,CAQWC,UAAaC,SARxB,CASH,CAdD,EAiBApB,QAAQ,CAACC,aAAT,CAAuB,cAAvB,EAAuC2B,KAAvC,CAA6CC,SAA7C,CAAyD,SAC5D,CApCM,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Report builder filter management\n *\n * @module core_reportbuilder/filters\n * @package core_reportbuilder\n * @copyright 2021 Paul Holden <[email protected]>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {dispatchEvent} from 'core/event_dispatcher';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport {get_string as getString} from 'core/str';\nimport {add as addToast} from 'core/toast';\nimport DynamicForm from 'core_form/dynamicform';\nimport * as reportEvents from 'core_reportbuilder/local/events';\nimport * as reportSelectors from 'core_reportbuilder/local/selectors';\nimport {reset as resetFilters} from 'core_reportbuilder/local/repository/filters';\n\n/**\n * Initialise module\n *\n * @param {Number} reportId\n */\nexport const init = reportId => {\n const reportElement = document.querySelector(reportSelectors.forSystemReport(reportId));\n const filterFormContainer = reportElement.querySelector(reportSelectors.regions.filtersForm);\n const filterForm = new DynamicForm(filterFormContainer, '\\\\core_reportbuilder\\\\form\\\\filter');\n\n // Submit report filters.\n filterForm.addEventListener(filterForm.events.FORM_SUBMITTED, event => {\n event.preventDefault();\n\n // After the form has been submitted, we should trigger report table reload.\n dispatchEvent(reportEvents.tableReload, {}, reportElement);\n\n getString('filtersapplied', 'core_reportbuilder')\n .then(addToast)\n .catch(Notification.exception);\n });\n\n // Reset report filters.\n filterForm.addEventListener(filterForm.events.NOSUBMIT_BUTTON_PRESSED, event => {\n event.preventDefault();\n\n const pendingPromise = new Pending('core_reportbuilder/filters:reset');\n\n resetFilters(reportId)\n .then(() => getString('filtersreset', 'core_reportbuilder'))\n .then(addToast)\n .then(() => {\n pendingPromise.resolve();\n window.location.reload();\n return;\n })\n .catch(Notification.exception);\n });\n\n // Modify \"region-main\" overflow for big filter forms.\n document.querySelector('#region-main').style.overflowX = \"visible\";\n};\n"],"file":"filters.min.js"}
+2
View File
@@ -0,0 +1,2 @@
define ("core_reportbuilder/local/events",["exports"],function(a){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;a.default={tableReload:"core_reportbuilder_table_reload"};return a.default});
//# sourceMappingURL=events.min.js.map
@@ -0,0 +1 @@
{"version":3,"sources":["../../src/local/events.js"],"names":["tableReload"],"mappings":"2JAwBe,CAGXA,WAAW,CAAE,iCAHF,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Report builder events\n *\n * @module core_reportbuilder/local/events\n * @package core_reportbuilder\n * @copyright 2021 Paul Holden <[email protected]>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nexport default {\n // Trigger table reloading, passing optional `preservePagination` details argument can be used to\n // define whether current pagination should be preserved (default is false, i.e. reload first page.)\n tableReload: 'core_reportbuilder_table_reload',\n};\n"],"file":"events.min.js"}
@@ -0,0 +1,2 @@
define ("core_reportbuilder/local/repository/filters",["exports","core/ajax"],function(a,b){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.reset=void 0;b=function(a){return a&&a.__esModule?a:{default:a}}(b);var c=function(a){return b.default.call([{methodname:"core_reportbuilder_filters_reset",args:{reportid:a}}])[0]};a.reset=c});
//# sourceMappingURL=filters.min.js.map
@@ -0,0 +1 @@
{"version":3,"sources":["../../../src/local/repository/filters.js"],"names":["reset","reportId","Ajax","call","methodname","args","reportid"],"mappings":"yKAwBA,uDAQO,GAAMA,CAAAA,CAAK,CAAG,SAAAC,CAAQ,CAAI,CAM7B,MAAOC,WAAKC,IAAL,CAAU,CALD,CACZC,UAAU,CAAE,kCADA,CAEZC,IAAI,CAAE,CAACC,QAAQ,CAAEL,CAAX,CAFM,CAKC,CAAV,EAAqB,CAArB,CACV,CAPM,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Module to handle filter AJAX requests\n *\n * @module core_reportbuilder/local/repository/filters\n * @package core_reportbuilder\n * @copyright 2021 Paul Holden <[email protected]>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\n\n/**\n * Reset all filters for given report\n *\n * @param {Number} reportId\n * @return {Promise}\n */\nexport const reset = reportId => {\n const request = {\n methodname: 'core_reportbuilder_filters_reset',\n args: {reportid: reportId}\n };\n\n return Ajax.call([request])[0];\n};\n"],"file":"filters.min.js"}
+2
View File
@@ -0,0 +1,2 @@
define ("core_reportbuilder/local/selectors",["exports"],function(a){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;var b={regions:{systemReport:"[data-region=\"core_reportbuilder/system-report\"]",filtersForm:"[data-region=\"filters-form\"]"}};b.forSystemReport=function(a){return"".concat(b.regions.systemReport,"[data-reportid=\"").concat(a,"\"]")};a.default=b;return a.default});
//# sourceMappingURL=selectors.min.js.map
@@ -0,0 +1 @@
{"version":3,"sources":["../../src/local/selectors.js"],"names":["SELECTORS","regions","systemReport","filtersForm","forSystemReport","reportId"],"mappings":"oJAwBA,GAAMA,CAAAA,CAAS,CAAG,CACdC,OAAO,CAAE,CACLC,YAAY,CAAE,oDADT,CAELC,WAAW,CAAE,gCAFR,CADK,CAAlB,CAOAH,CAAS,CAACI,eAAV,CAA4B,SAAAC,CAAQ,kBAAOL,CAAS,CAACC,OAAV,CAAkBC,YAAzB,6BAAwDG,CAAxD,QAApC,C,UAEeL,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Report builder selectors\n *\n * @module core_reportbuilder/local/selectors\n * @package core_reportbuilder\n * @copyright 2021 Paul Holden <[email protected]>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nconst SELECTORS = {\n regions: {\n systemReport: '[data-region=\"core_reportbuilder/system-report\"]',\n filtersForm: '[data-region=\"filters-form\"]',\n },\n};\n\nSELECTORS.forSystemReport = reportId => `${SELECTORS.regions.systemReport}[data-reportid=\"${reportId}\"]`;\n\nexport default SELECTORS;\n"],"file":"selectors.min.js"}
+2
View File
@@ -0,0 +1,2 @@
function _typeof(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){_typeof=function(a){return typeof a}}else{_typeof=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return _typeof(a)}define ("core_reportbuilder/report",["exports","core_reportbuilder/local/events","core_reportbuilder/local/selectors","core_table/dynamic","core_table/local/dynamic/selectors"],function(a,b,c,d,e){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;b=g(b);c=g(c);e=g(e);function f(){if("function"!=typeof WeakMap)return null;var a=new WeakMap;f=function(){return a};return a}function g(a){if(a&&a.__esModule){return a}if(null===a||"object"!==_typeof(a)&&"function"!=typeof a){return{default:a}}var b=f();if(b&&b.has(a)){return b.get(a)}var c={},d=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var e in a){if(Object.prototype.hasOwnProperty.call(a,e)){var g=d?Object.getOwnPropertyDescriptor(a,e):null;if(g&&(g.get||g.set)){Object.defineProperty(c,e,g)}else{c[e]=a[e]}}}c.default=a;if(b){b.set(a,c)}return c}function h(a,b,c,d,e,f,g){try{var h=a[f](g),i=h.value}catch(a){c(a);return}if(h.done){b(i)}else{Promise.resolve(i).then(d,e)}}function i(a){return function(){var b=this,c=arguments;return new Promise(function(d,e){var i=a.apply(b,c);function f(a){h(i,d,e,f,g,"next",a)}function g(a){h(i,d,e,f,g,"throw",a)}f(void 0)})}}var j=function(a){document.addEventListener(b.tableReload,function(){var b=i(regeneratorRuntime.mark(function b(f){var g,h,i,j;return regeneratorRuntime.wrap(function(b){while(1){switch(b.prev=b.next){case 0:h=f.target.closest(c.forSystemReport(a));if(!(null===h)){b.next=3;break}return b.abrupt("return");case 3:i=h.querySelector(e.main.region);j=(null===(g=f.detail)||void 0===g?void 0:g.preservePagination)?null:1;b.next=7;return(0,d.setPageNumber)(i,j,!1).then(d.refreshTableContent);case 7:case"end":return b.stop();}}},b)}));return function(){return b.apply(this,arguments)}}())};a.init=j});
//# sourceMappingURL=report.min.js.map
@@ -0,0 +1 @@
{"version":3,"sources":["../src/report.js"],"names":["init","reportId","document","addEventListener","reportEvents","tableReload","event","triggerElement","target","closest","reportSelectors","forSystemReport","tableRoot","querySelector","tableSelectors","main","region","pageNumber","detail","preservePagination","then","refreshTableContent"],"mappings":"ojBAwBA,OACA,OAEA,O,q2BAOO,GAAMA,CAAAA,CAAI,CAAG,SAAAC,CAAQ,CAAI,CAE5BC,QAAQ,CAACC,gBAAT,CAA0BC,CAAY,CAACC,WAAvC,4CAAoD,WAAMC,CAAN,+FAC1CC,CAD0C,CACzBD,CAAK,CAACE,MAAN,CAAaC,OAAb,CAAqBC,CAAe,CAACC,eAAhB,CAAgCV,CAAhC,CAArB,CADyB,MAEzB,IAAnB,GAAAM,CAF4C,mDAM1CK,CAN0C,CAM9BL,CAAc,CAACM,aAAf,CAA6BC,CAAc,CAACC,IAAf,CAAoBC,MAAjD,CAN8B,CAO1CC,CAP0C,CAO7B,WAAAX,CAAK,CAACY,MAAN,uBAAcC,kBAAd,EAAmC,IAAnC,CAA0C,CAPb,gBAS1C,oBAAcP,CAAd,CAAyBK,CAAzB,KACDG,IADC,CACIC,qBADJ,CAT0C,yCAApD,wDAYH,CAdM,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Report builder report management\n *\n * @module core_reportbuilder/report\n * @package core_reportbuilder\n * @copyright 2021 Paul Holden <[email protected]>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport * as reportEvents from 'core_reportbuilder/local/events';\nimport * as reportSelectors from 'core_reportbuilder/local/selectors';\nimport {setPageNumber, refreshTableContent} from 'core_table/dynamic';\nimport * as tableSelectors from 'core_table/local/dynamic/selectors';\n\n/**\n * Initialise module\n *\n * @param {Number} reportId\n */\nexport const init = reportId => {\n // Listen for the table reload event.\n document.addEventListener(reportEvents.tableReload, async(event) => {\n const triggerElement = event.target.closest(reportSelectors.forSystemReport(reportId));\n if (triggerElement === null) {\n return;\n }\n\n const tableRoot = triggerElement.querySelector(tableSelectors.main.region);\n const pageNumber = event.detail?.preservePagination ? null : 1;\n\n await setPageNumber(tableRoot, pageNumber, false)\n .then(refreshTableContent);\n });\n};\n"],"file":"report.min.js"}
+76
View File
@@ -0,0 +1,76 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Report builder filter management
*
* @module core_reportbuilder/filters
* @package core_reportbuilder
* @copyright 2021 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {dispatchEvent} from 'core/event_dispatcher';
import Notification from 'core/notification';
import Pending from 'core/pending';
import {get_string as getString} from 'core/str';
import {add as addToast} from 'core/toast';
import DynamicForm from 'core_form/dynamicform';
import * as reportEvents from 'core_reportbuilder/local/events';
import * as reportSelectors from 'core_reportbuilder/local/selectors';
import {reset as resetFilters} from 'core_reportbuilder/local/repository/filters';
/**
* Initialise module
*
* @param {Number} reportId
*/
export const init = reportId => {
const reportElement = document.querySelector(reportSelectors.forSystemReport(reportId));
const filterFormContainer = reportElement.querySelector(reportSelectors.regions.filtersForm);
const filterForm = new DynamicForm(filterFormContainer, '\\core_reportbuilder\\form\\filter');
// Submit report filters.
filterForm.addEventListener(filterForm.events.FORM_SUBMITTED, event => {
event.preventDefault();
// After the form has been submitted, we should trigger report table reload.
dispatchEvent(reportEvents.tableReload, {}, reportElement);
getString('filtersapplied', 'core_reportbuilder')
.then(addToast)
.catch(Notification.exception);
});
// Reset report filters.
filterForm.addEventListener(filterForm.events.NOSUBMIT_BUTTON_PRESSED, event => {
event.preventDefault();
const pendingPromise = new Pending('core_reportbuilder/filters:reset');
resetFilters(reportId)
.then(() => getString('filtersreset', 'core_reportbuilder'))
.then(addToast)
.then(() => {
pendingPromise.resolve();
window.location.reload();
return;
})
.catch(Notification.exception);
});
// Modify "region-main" overflow for big filter forms.
document.querySelector('#region-main').style.overflowX = "visible";
};
+29
View File
@@ -0,0 +1,29 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Report builder events
*
* @module core_reportbuilder/local/events
* @package core_reportbuilder
* @copyright 2021 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
export default {
// Trigger table reloading, passing optional `preservePagination` details argument can be used to
// define whether current pagination should be preserved (default is false, i.e. reload first page.)
tableReload: 'core_reportbuilder_table_reload',
};
@@ -0,0 +1,40 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Module to handle filter AJAX requests
*
* @module core_reportbuilder/local/repository/filters
* @package core_reportbuilder
* @copyright 2021 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Ajax from 'core/ajax';
/**
* Reset all filters for given report
*
* @param {Number} reportId
* @return {Promise}
*/
export const reset = reportId => {
const request = {
methodname: 'core_reportbuilder_filters_reset',
args: {reportid: reportId}
};
return Ajax.call([request])[0];
};
+34
View File
@@ -0,0 +1,34 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Report builder selectors
*
* @module core_reportbuilder/local/selectors
* @package core_reportbuilder
* @copyright 2021 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
const SELECTORS = {
regions: {
systemReport: '[data-region="core_reportbuilder/system-report"]',
filtersForm: '[data-region="filters-form"]',
},
};
SELECTORS.forSystemReport = reportId => `${SELECTORS.regions.systemReport}[data-reportid="${reportId}"]`;
export default SELECTORS;
+49
View File
@@ -0,0 +1,49 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Report builder report management
*
* @module core_reportbuilder/report
* @package core_reportbuilder
* @copyright 2021 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import * as reportEvents from 'core_reportbuilder/local/events';
import * as reportSelectors from 'core_reportbuilder/local/selectors';
import {setPageNumber, refreshTableContent} from 'core_table/dynamic';
import * as tableSelectors from 'core_table/local/dynamic/selectors';
/**
* Initialise module
*
* @param {Number} reportId
*/
export const init = reportId => {
// Listen for the table reload event.
document.addEventListener(reportEvents.tableReload, async(event) => {
const triggerElement = event.target.closest(reportSelectors.forSystemReport(reportId));
if (triggerElement === null) {
return;
}
const tableRoot = triggerElement.querySelector(tableSelectors.main.region);
const pageNumber = event.detail?.preservePagination ? null : 1;
await setPageNumber(tableRoot, pageNumber, false)
.then(refreshTableContent);
});
};
+79
View File
@@ -0,0 +1,79 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\external\filters;
use context_system;
use external_api;
use external_function_parameters;
use external_value;
use core_reportbuilder\local\helpers\user_filter_manager;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once("{$CFG->libdir}/externallib.php");
/**
* External method for resetting report filters
*
* @package core_reportbuilder
* @copyright 2021 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class reset extends external_api {
/**
* External method parameters
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'reportid' => new external_value(PARAM_INT, 'Report ID'),
]);
}
/**
* External method execution
*
* @param int $reportid
* @return bool
*/
public static function execute(int $reportid): bool {
[
'reportid' => $reportid,
] = self::validate_parameters(self::execute_parameters(), [
'reportid' => $reportid,
]);
$context = context_system::instance();
self::validate_context($context);
return user_filter_manager::reset_all($reportid);
}
/**
* External method return value
*
* @return external_value
*/
public static function execute_returns(): external_value {
return new external_value(PARAM_BOOL, 'Success');
}
}
@@ -0,0 +1,115 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\external;
use core\external\persistent_exporter;
use core_reportbuilder\form\filter;
use core_reportbuilder\local\models\report;
use core_reportbuilder\local\report\base;
use core_reportbuilder\table\system_report_table;
use core_reportbuilder\table\system_report_table_filterset;
use core_reportbuilder\table\system_report_table_parameters_filter;
use renderer_base;
/**
* Report exporter class
*
* @package core_reportbuilder
* @copyright 2020 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class system_report_exporter extends persistent_exporter {
/**
* Return the name of the class we are exporting
*
* @return string
*/
protected static function define_class(): string {
return report::class;
}
/**
* Return a list of objects that are related to the persistent
*
* @return array
*/
protected static function define_related(): array {
return [
'source' => base::class,
'parameters' => 'string',
];
}
/**
* Return a list of additional properties used only for display
*
* @return array
*/
protected static function define_other_properties(): array {
return [
'table' => ['type' => PARAM_RAW],
'parameters' => ['type' => PARAM_RAW],
'filterspresent' => ['type' => PARAM_BOOL],
'filtersform' => [
'type' => PARAM_RAW,
'optional' => true,
],
];
}
/**
* Get additional values to inject while exporting
*
* @uses \core_reportbuilder\output\renderer::render_system_report_table()
*
* @param renderer_base $output
* @return array
*/
protected function get_other_values(renderer_base $output): array {
/** @var base $source */
$source = $this->related['source'];
/** @var array $parameters */
$parameters = $this->related['parameters'];
$filterset = new system_report_table_filterset();
$filterset->add_filter(new system_report_table_parameters_filter('parameters', null, [$parameters]));
$table = system_report_table::create($this->persistent->get('id'), (array) json_decode($parameters, true));
$table->set_filterset($filterset);
// Generate filters form if report contains any filters.
$filterspresent = !empty($source->get_filters());
if ($filterspresent) {
$filtersform = new filter(null, null, 'post', '', [], true, [
'reportid' => $this->persistent->get('id'),
'parameters' => $parameters,
]);
$filtersform->set_data_for_dynamic_submission();
}
return [
'table' => $output->render($table),
'parameters' => $this->related['parameters'],
'filterspresent' => $filterspresent,
'filtersform' => $filterspresent ? $filtersform->render() : '',
];
}
}
+144
View File
@@ -0,0 +1,144 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\form;
use context;
use moodle_url;
use core_form\dynamic_form;
use core_reportbuilder\manager;
use core_reportbuilder\system_report;
use core_reportbuilder\local\models\report;
/**
* Dynamic filter form
*
* @package core_reportbuilder
* @copyright 2021 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class filter extends dynamic_form {
/**
* Return instance of the system report using the filter form
*
* @return system_report
*/
private function get_system_report(): system_report {
$report = new report($this->optional_param('reportid', 0, PARAM_INT));
$parameters = (array) json_decode($this->optional_param('parameters', '', PARAM_RAW));
/** @var system_report $systemreport */
$systemreport = manager::get_report_from_persistent($report, $parameters);
return $systemreport;
}
/**
* Return the context for the form, it should be that of the system report itself
*
* @return context
*/
protected function get_context_for_dynamic_submission(): context {
return ($this->get_system_report())->get_context();
}
/**
* Ensure current user is able to use this form
*
* A {@see \core_reportbuilder\report_access_exception} will be thrown if they can't
*/
protected function check_access_for_dynamic_submission(): void {
$this->get_system_report()->require_can_view();
}
/**
* Process the form submission
*
* @return bool
*/
public function process_dynamic_submission() {
$values = $this->get_data();
// Remove some unneeded fields.
unset($values->reportid, $values->parameters);
return $this->get_system_report()->set_filter_values((array) $values);
}
/**
* Load in existing data as form defaults
*/
public function set_data_for_dynamic_submission(): void {
$defaults = [
'reportid' => $this->optional_param('reportid', 0, PARAM_INT),
'parameters' => $this->optional_param('parameters', 0, PARAM_RAW),
];
$this->set_data(array_merge($defaults, $this->get_system_report()->get_filter_values()));
}
/**
* URL of the page using this form
*
* @return moodle_url
*/
protected function get_page_url_for_dynamic_submission(): moodle_url {
return new moodle_url('/');
}
/**
* Filter form definition. It should provide necessary field itself, then allow all report filters to add their own elements
*/
protected function definition() {
global $OUTPUT;
$mform = $this->_form;
$mform->addElement('hidden', 'reportid');
$mform->setType('reportid', PARAM_INT);
$mform->addElement('hidden', 'parameters');
$mform->setType('parameters', PARAM_RAW);
// Allow each filter instance to add itself to this form, wrapping each inside custom header/footer template.
foreach ($this->get_system_report()->get_filter_instances() as $filterinstance) {
$mform->addElement('html', $OUTPUT->render_from_template('core_reportbuilder/local/filters/header', [
'name' => $filterinstance->get_header(),
]));
$filterinstance->setup_form($mform);
$mform->addElement('html', $OUTPUT->render_from_template('core_reportbuilder/local/filters/footer', []));
}
$this->set_display_vertical();
// We'll add a second submit button to the form that will be used to reset current report filters.
$mform->registerNoSubmitButton('resetfilters');
$buttons = [];
$buttons[] = $mform->createElement('submit', 'submitbutton', get_string('apply', 'core_reportbuilder'));
$buttons[] = $mform->createElement('submit', 'resetfilters', get_string('resetall', 'core_reportbuilder'),
null, null, ['customclassoverride' => 'btn-link']);
$mform->addGroup($buttons, 'buttonar', '', [' '], false);
$mform->closeHeaderBefore('buttonar');
$mform->disable_form_change_checker();
}
}
@@ -0,0 +1,273 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\local\entities;
use coding_exception;
use lang_string;
use core_reportbuilder\local\report\column;
use core_reportbuilder\local\report\filter;
/**
* Base class for all report entities
*
* @package core_reportbuilder
* @copyright 2019 Marina Glancy <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class base {
/** @var string $entityname Internal reference to name of entity */
private $entityname = '';
/** @var lang_string $entitytitle Used as a title for the entity in reports */
private $entitytitle = null;
/** @var array $tablealiases Database tables that this entity uses and their default aliases */
private $tablealiases = [];
/** @var string[] $joins List of SQL joins for the entity */
private $joins = [];
/** @var column[] $columns List of columns for the entity */
private $columns = [];
/** @var filter[] $filters List of filters for the entity */
private $filters = [];
/**
* Database tables that this entity uses and their default aliases
*
* Must be overridden by the entity to list all database tables that it expects to be present in the main
* SQL or in JOINs added to this entity
*
* @return string[] Array of $tablename => $alias
*/
abstract protected function get_default_table_aliases(): array;
/**
* The default title for this entity
*
* @return lang_string
*/
abstract protected function get_default_entity_title(): lang_string;
/**
* Initialise the entity, called automatically when it is added to a report
*
* This is where entity defines all its columns and filters by calling:
* - {@see add_column}
* - {@see add_filter}
* - etc
*
* @return self
*/
abstract public function initialise(): self;
/**
* The default machine-readable name for this entity that will be used in the internal names of the columns/filters
*
* @return string
*/
protected function get_default_entity_name(): string {
$namespace = explode('\\', get_called_class());
return end($namespace);
}
/**
* Set entity name
*
* @param string $entityname
* @return self
* @throws coding_exception
*/
final public function set_entity_name(string $entityname): self {
if ($entityname === '' || $entityname !== clean_param($entityname, PARAM_ALPHANUMEXT)) {
throw new coding_exception('Entity name must be comprised of alphanumeric character, underscore or dash');
}
$this->entityname = $entityname;
return $this;
}
/**
* Return entity name
*
* @return string
*/
final public function get_entity_name(): string {
return $this->entityname ?: $this->get_default_entity_name();
}
/**
* Set entity title
*
* @param lang_string $title
* @return self
*/
final public function set_entity_title(lang_string $title): self {
$this->entitytitle = $title;
return $this;
}
/**
* Get entity title
*
* @return lang_string
*/
final public function get_entity_title(): lang_string {
return $this->entitytitle ?? $this->get_default_entity_title();
}
/**
* Override the default alias for given database table used in entity queries
*
* @param string $tablename
* @param string $alias
* @return self
* @throws coding_exception
*/
final public function set_table_alias(string $tablename, string $alias): self {
if (!array_key_exists($tablename, $this->get_default_table_aliases())) {
throw new coding_exception('Invalid table name', $tablename);
}
$this->tablealiases[$tablename] = $alias;
return $this;
}
/**
* Returns an alias used in the queries for a given table
*
* @param string $tablename
* @return string
* @throws coding_exception
*/
final public function get_table_alias(string $tablename): string {
$defaulttablealiases = $this->get_default_table_aliases();
if (!array_key_exists($tablename, $defaulttablealiases)) {
throw new coding_exception('Invalid table name', $tablename);
}
return $this->tablealiases[$tablename] ?? $defaulttablealiases[$tablename];
}
/**
* Add join clause required for this entity to join to existing tables/entities
*
* @param string $join
* @return self
*/
final public function add_join(string $join): self {
$this->joins[trim($join)] = trim($join);
return $this;
}
/**
* Add multiple join clauses required for this entity {@see add_join}
*
* @param string[] $joins
* @return self
*/
final public function add_joins(array $joins): self {
foreach ($joins as $join) {
$this->add_join($join);
}
return $this;
}
/**
* Return entity joins
*
* @return string[]
*/
final protected function get_joins(): array {
return array_values($this->joins);
}
/**
* Add a column to the entity
*
* @param column $column
* @return self
*/
final protected function add_column(column $column): self {
$this->columns[$column->get_name()] = $column;
return $this;
}
/**
* Returns entity columns
*
* @return column[]
*/
final public function get_columns(): array {
return $this->columns;
}
/**
* Returns an entity column
*
* @param string $name
* @return column
* @throws coding_exception For invalid column name
*/
final public function get_column(string $name): column {
if (!array_key_exists($name, $this->columns)) {
throw new coding_exception('Invalid column name', $name);
}
return $this->columns[$name];
}
/**
* Add a filter to the entity
*
* @param filter $filter
* @return $this
*/
final protected function add_filter(filter $filter): self {
$this->filters[$filter->get_name()] = $filter;
return $this;
}
/**
* Returns entity filters
*
* @return filter[]
*/
final public function get_filters(): array {
return $this->filters;
}
/**
* Returns an entity filter
*
* @param string $name
* @return filter
* @throws coding_exception For invalid filter name
*/
final public function get_filter(string $name): filter {
if (!array_key_exists($name, $this->filters)) {
throw new coding_exception('Invalid filter name', $name);
}
return $this->filters[$name];
}
}
@@ -0,0 +1,464 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\local\entities;
use context_course;
use context_helper;
use core_course_category;
use core_reportbuilder\local\filters\boolean_select;
use core_reportbuilder\local\filters\course_selector;
use core_reportbuilder\local\filters\date;
use core_reportbuilder\local\filters\select;
use core_reportbuilder\local\filters\text;
use core_reportbuilder\local\helpers\custom_fields;
use core_reportbuilder\local\helpers\format;
use core_reportbuilder\local\report\column;
use core_reportbuilder\local\report\filter;
use html_writer;
use lang_string;
use stdClass;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/course/lib.php');
/**
* Course entity class implementation
*
* This entity defines all the course columns and filters to be used in any report.
*
* @package core_reportbuilder
* @copyright 2021 Sara Arjona <[email protected]> based on Marina Glancy code.
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course extends base {
/**
* Database tables that this entity uses and their default aliases.
*
* @return array
*/
protected function get_default_table_aliases(): array {
return [
'course' => 'c',
'context' => 'cctx',
];
}
/**
* The default machine-readable name for this entity that will be used in the internal names of the columns/filters.
*
* @return string
*/
protected function get_default_entity_name(): string {
return 'course';
}
/**
* The default title for this entity in the list of columns/filters in the report builder.
*
* @return lang_string
*/
protected function get_default_entity_title(): lang_string {
return new lang_string('entitycourse', 'core_reportbuilder');
}
/**
* Get custom fields helper
*
* @return custom_fields
*/
protected function get_custom_fields(): custom_fields {
$customfields = new custom_fields($this->get_table_alias('course') . '.id', $this->get_entity_name(),
'core_course', 'course');
$customfields->add_joins($this->get_joins());
return $customfields;
}
/**
* Initialise the entity, adding all course and custom course fields
*
* @return base
*/
public function initialise(): base {
$customfields = $this->get_custom_fields();
$columns = array_merge($this->get_all_columns(), $customfields->get_columns());
foreach ($columns as $column) {
$this->add_column($column);
}
$filters = array_merge($this->get_all_filters(), $customfields->get_filters());
foreach ($filters as $filter) {
$this->add_filter($filter);
}
return $this;
}
/**
* Course fields.
*
* @return array
*/
protected function get_course_fields(): array {
return [
'fullname' => new lang_string('fullnamecourse'),
'shortname' => new lang_string('shortnamecourse'),
'category' => new lang_string('coursecategory'),
'idnumber' => new lang_string('idnumbercourse'),
'summary' => new lang_string('coursesummary'),
'format' => new lang_string('format'),
'startdate' => new lang_string('startdate'),
'enddate' => new lang_string('enddate'),
'visible' => new lang_string('coursevisibility'),
'groupmode' => new lang_string('groupmode', 'group'),
'groupmodeforce' => new lang_string('groupmodeforce', 'group'),
'lang' => new lang_string('forcelanguage'),
'calendartype' => new lang_string('forcecalendartype', 'calendar'),
'theme' => new lang_string('forcetheme'),
'enablecompletion' => new lang_string('enablecompletion', 'completion'),
'downloadcontent' => new lang_string('downloadcoursecontent', 'course'),
];
}
/**
* Check if this field is sortable
*
* @param string $fieldname
* @return bool
*/
protected function is_sortable(string $fieldname): bool {
// Some columns can't be sorted, like longtext or images.
$nonsortable = [
'summary',
];
return !in_array($fieldname, $nonsortable);
}
/**
* Return appropriate column type for given user field
*
* @param string $coursefield
* @return int
*/
protected function get_course_field_type(string $coursefield): int {
switch ($coursefield) {
case 'downloadcontent':
case 'enablecompletion':
case 'groupmodeforce':
case 'visible':
$fieldtype = column::TYPE_BOOLEAN;
break;
case 'startdate':
case 'enddate':
$fieldtype = column::TYPE_TIMESTAMP;
break;
case 'summary':
$fieldtype = column::TYPE_LONGTEXT;
break;
case 'category':
case 'groupmode':
$fieldtype = column::TYPE_INTEGER;
break;
case 'calendartype':
case 'idnumber':
case 'format':
case 'fullname':
case 'lang':
case 'shortname':
case 'theme':
default:
$fieldtype = column::TYPE_TEXT;
break;
}
return $fieldtype;
}
/**
* Returns list of all available columns.
*
* These are all the columns available to use in any report that uses this entity.
*
* @return column[]
*/
protected function get_all_columns(): array {
$columns = [];
$coursefields = $this->get_course_fields();
$tablealias = $this->get_table_alias('course');
$contexttablealias = $this->get_table_alias('context');
// Columns course full name with link, course short name with link and course id with link.
$fields = [
'coursefullnamewithlink' => 'fullname',
'courseshortnamewithlink' => 'shortname',
'courseidnumberewithlink' => 'idnumber',
];
foreach ($fields as $key => $field) {
$columns[] = (new column(
$key,
new lang_string($key, 'core_reportbuilder'),
$this->get_entity_name()
))
->add_joins($this->get_joins())
->set_type(column::TYPE_TEXT)
->add_fields("{$tablealias}.{$field} as $key, {$tablealias}.id")
->set_is_sortable(true)
->add_callback(static function(?string $value, stdClass $row): string {
if ($value === null) {
return '';
}
return html_writer::link(course_get_url($row->id), $value);
});
}
foreach ($coursefields as $coursefield => $coursefieldlang) {
$column = (new column(
$coursefield,
$coursefieldlang,
$this->get_entity_name()
))
->add_joins($this->get_joins())
->set_type($this->get_course_field_type($coursefield))
->add_field("$tablealias.$coursefield")
->add_callback([$this, 'format'], $coursefield)
->set_is_sortable($this->is_sortable($coursefield));
// Join on the context table so that we can use it for formatting these columns later.
if ($coursefield === 'summary' || $coursefield === 'shortname' || $coursefield === 'fullname') {
$join = "LEFT JOIN {context} {$contexttablealias}
ON {$contexttablealias}.contextlevel = " . CONTEXT_COURSE . "
AND {$contexttablealias}.instanceid = {$tablealias}.id";
$column->add_join($join)
->add_field("{$tablealias}.id", 'courseid')
->add_fields(context_helper::get_preload_record_columns_sql($contexttablealias));
}
$columns[] = $column;
}
return $columns;
}
/**
* Returns list of all available filters
*
* @return array
*/
protected function get_all_filters(): array {
global $DB;
$filters = [];
$tablealias = $this->get_table_alias('course');
$fields = $this->get_course_fields();
foreach ($fields as $field => $name) {
// Filtering isn't supported for LONGTEXT fields on Oracle.
if ($this->get_course_field_type($field) === column::TYPE_LONGTEXT &&
$DB->get_dbfamily() === 'oracle') {
continue;
}
$optionscallback = [static::class, 'get_options_for_' . $field];
if (is_callable($optionscallback)) {
$filterclass = select::class;
} else if ($this->get_course_field_type($field) === column::TYPE_BOOLEAN) {
$filterclass = boolean_select::class;
} else if ($this->get_course_field_type($field) === column::TYPE_TIMESTAMP) {
$filterclass = date::class;
} else {
$filterclass = text::class;
}
$filter = (new filter(
$filterclass,
$field,
$name,
$this->get_entity_name(),
"{$tablealias}.$field"
))
->add_joins($this->get_joins());
// Populate filter options by callback, if available.
if (is_callable($optionscallback)) {
$filter->set_options_callback($optionscallback);
}
$filters[] = $filter;
}
// We add our own custom course selector filter.
$filters[] = (new filter(
course_selector::class,
'courseselector',
new lang_string('courses'),
$this->get_entity_name(),
"{$tablealias}.id"
))
->add_joins($this->get_joins());
return $filters;
}
/**
* Gets list of options if the filter supports it
*
* @param string $fieldname
* @return null|array
*/
protected function get_options_for(string $fieldname): ?array {
static $cached = [];
if (!array_key_exists($fieldname, $cached)) {
$callable = [static::class, 'get_options_for_' . $fieldname];
if (is_callable($callable)) {
$cached[$fieldname] = $callable();
} else {
$cached[$fieldname] = null;
}
}
return $cached[$fieldname];
}
/**
* List of options for the field groupmode.
*
* @return array
*/
public static function get_options_for_groupmode(): array {
return [
NOGROUPS => get_string('groupsnone', 'group'),
SEPARATEGROUPS => get_string('groupsseparate', 'group'),
VISIBLEGROUPS => get_string('groupsvisible', 'group'),
];
}
/**
* List of options for the field category.
*
* @return array
*/
public static function get_options_for_category(): array {
return core_course_category::make_categories_list('moodle/category:viewcourselist');
}
/**
* List of options for the field format.
*
* @return array
*/
public static function get_options_for_format(): array {
global $CFG;
require_once($CFG->dirroot.'/course/lib.php');
$options = [];
$courseformats = get_sorted_course_formats(true);
foreach ($courseformats as $courseformat) {
$options[$courseformat] = get_string('pluginname', "format_{$courseformat}");
}
return $options;
}
/**
* List of options for the field theme.
*
* @return array
*/
public static function get_options_for_theme(): array {
$options = [];
$themeobjects = get_list_of_themes();
foreach ($themeobjects as $key => $theme) {
if (empty($theme->hidefromselector)) {
$options[$key] = get_string('pluginname', "theme_{$theme->name}");
}
}
return $options;
}
/**
* List of options for the field lang.
*
* @return array
*/
public static function get_options_for_lang(): array {
return get_string_manager()->get_list_of_translations();
}
/**
* List of options for the field.
*
* @return array
*/
public static function get_options_for_calendartype(): array {
return \core_calendar\type_factory::get_list_of_calendar_types();
}
/**
* Formats the course field for display.
*
* @param mixed $value Current field value.
* @param stdClass $row Complete row.
* @param string $fieldname Name of the field to format.
* @return string
*/
public function format($value, stdClass $row, string $fieldname): string {
if ($this->get_course_field_type($fieldname) === column::TYPE_TIMESTAMP) {
return format::userdate($value, $row);
}
$options = $this->get_options_for($fieldname);
if ($options !== null && array_key_exists($value, $options)) {
return $options[$value];
}
if ($this->get_course_field_type($fieldname) === column::TYPE_BOOLEAN) {
return format::boolean_as_text($value);
}
if (in_array($fieldname, ['fullname', 'shortname'])) {
if (!$row->courseid) {
return '';
}
context_helper::preload_from_record($row);
$context = context_course::instance($row->courseid);
return format_string($value, true, ['context' => $context->id, 'escape' => false]);
}
if (in_array($fieldname, ['summary'])) {
if (!$row->courseid) {
return '';
}
context_helper::preload_from_record($row);
$context = context_course::instance($row->courseid);
$summary = file_rewrite_pluginfile_urls($row->summary, 'pluginfile.php', $context->id, 'course', 'summary', null);
return format_text($summary);
}
return s($value);
}
}
@@ -0,0 +1,396 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\local\entities;
use context_system;
use html_writer;
use lang_string;
use moodle_url;
use stdClass;
use core_user\fields;
use core_reportbuilder\local\filters\boolean_select;
use core_reportbuilder\local\filters\date;
use core_reportbuilder\local\filters\select;
use core_reportbuilder\local\filters\text;
use core_reportbuilder\local\helpers\user_profile_fields;
use core_reportbuilder\local\helpers\format;
use core_reportbuilder\local\report\column;
use core_reportbuilder\local\report\filter;
/**
* User entity class implementation.
*
* This entity defines all the user columns and filters to be used in any report.
*
* @package core_reportbuilder
* @copyright 2020 Sara Arjona <[email protected]> based on Marina Glancy code.
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class user extends base {
/**
* Database tables that this entity uses and their default aliases
*
* @return array
*/
protected function get_default_table_aliases(): array {
return ['user' => 'u'];
}
/**
* The default title for this entity
*
* @return lang_string
*/
protected function get_default_entity_title(): lang_string {
return new lang_string('entityuser', 'core_reportbuilder');
}
/**
* Initialise the entity, add all user fields and all 'visible' user profile fields
*
* @return base
*/
public function initialise(): base {
$userprofilefields = $this->get_user_profile_fields();
$columns = array_merge($this->get_all_columns(), $userprofilefields->get_columns());
foreach ($columns as $column) {
$this->add_column($column);
}
$filters = array_merge($this->get_all_filters(), $userprofilefields->get_filters());
foreach ($filters as $filter) {
$this->add_filter($filter);
}
return $this;
}
/**
* Get user profile fields helper instance
*
* @return user_profile_fields
*/
protected function get_user_profile_fields(): user_profile_fields {
$userprofilefields = new user_profile_fields($this->get_table_alias('user') . '.id', $this->get_entity_name());
$userprofilefields->add_joins($this->get_joins());
return $userprofilefields;
}
/**
* Returns list of all available columns
*
* These are all the columns available to use in any report that uses this entity.
*
* @return column[]
*/
protected function get_all_columns(): array {
$usertablealias = $this->get_table_alias('user');
$fullnameselect = self::get_name_fields_select($usertablealias);
$userpictureselect = fields::for_userpic()->get_sql($usertablealias, false, '', '', false)->selects;
$viewfullnames = has_capability('moodle/site:viewfullnames', context_system::instance());
// Fullname column.
$columns[] = (new column(
'fullname',
new lang_string('fullname'),
$this->get_entity_name()
))
->add_joins($this->get_joins())
->add_fields($fullnameselect)
->set_type(column::TYPE_TEXT)
->set_is_sortable($this->is_sortable('fullname'))
->add_callback(static function(?string $value, stdClass $row) use ($viewfullnames): string {
if ($value === null) {
return '';
}
return fullname($row, $viewfullnames);
});
// Formatted fullname columns (with link, picture or both).
$fullnamefields = [
'fullnamewithlink' => new lang_string('userfullnamewithlink', 'core_reportbuilder'),
'fullnamewithpicture' => new lang_string('userfullnamewithpicture', 'core_reportbuilder'),
'fullnamewithpicturelink' => new lang_string('userfullnamewithpicturelink', 'core_reportbuilder'),
];
foreach ($fullnamefields as $fullnamefield => $fullnamelang) {
$column = (new column(
$fullnamefield,
$fullnamelang,
$this->get_entity_name()
))
->add_joins($this->get_joins())
->add_fields($fullnameselect)
->add_field("{$usertablealias}.id")
->set_type(column::TYPE_TEXT)
->set_is_sortable($this->is_sortable($fullnamefield))
->add_callback(static function(?string $value, stdClass $row) use ($fullnamefield, $viewfullnames): string {
global $OUTPUT;
if ($value === null) {
return '';
}
if ($fullnamefield === 'fullnamewithlink') {
return html_writer::link(new moodle_url('/user/profile.php', ['id' => $row->id]),
fullname($row, $viewfullnames));
}
if ($fullnamefield === 'fullnamewithpicture') {
return $OUTPUT->user_picture($row, ['link' => false, 'alttext' => false]) .
fullname($row, $viewfullnames);
}
if ($fullnamefield === 'fullnamewithpicturelink') {
return html_writer::link(new moodle_url('/user/profile.php', ['id' => $row->id]),
$OUTPUT->user_picture($row, ['link' => false, 'alttext' => false]) .
fullname($row, $viewfullnames));
}
return $value;
});
// Picture fields need some more data.
if (strpos($fullnamefield, 'picture') !== false) {
$column->add_fields($userpictureselect);
}
$columns[] = $column;
}
// Picture column.
$columns[] = (new column(
'picture',
new lang_string('userpicture', 'core_reportbuilder'),
$this->get_entity_name()
))
->add_joins($this->get_joins())
->add_fields($userpictureselect)
->set_type(column::TYPE_INTEGER)
->set_is_sortable($this->is_sortable('picture'))
->add_callback(static function (int $value, stdClass $row): string {
global $OUTPUT;
return !empty($row->id) ? $OUTPUT->user_picture($row, ['link' => false, 'alttext' => false]) : '';
});
// Add all other user fields.
$userfields = $this->get_user_fields();
foreach ($userfields as $userfield => $userfieldlang) {
$columntype = $this->get_user_field_type($userfield);
$column = (new column(
$userfield,
$userfieldlang,
$this->get_entity_name()
))
->add_joins($this->get_joins())
->add_field("{$usertablealias}.{$userfield}")
->set_type($columntype)
->set_is_sortable($this->is_sortable($userfield))
->add_callback([$this, 'format'], $userfield);
// Some columns also have specific format callbacks.
if ($userfield === 'country') {
$column->add_callback(static function(string $country): string {
$countries = get_string_manager()->get_list_of_countries(true);
return $countries[$country] ?? '';
});
}
$columns[] = $column;
}
return $columns;
}
/**
* Check if this field is sortable
*
* @param string $fieldname
* @return bool
*/
protected function is_sortable(string $fieldname): bool {
// Some columns can't be sorted, like longtext or images.
$nonsortable = [
'picture',
];
return !in_array($fieldname, $nonsortable);
}
/**
* Formats the user field for display.
*
* @param mixed $value Current field value.
* @param stdClass $row Complete row.
* @param string $fieldname Name of the field to format.
* @return string
*/
public function format($value, stdClass $row, string $fieldname): string {
if ($this->get_user_field_type($fieldname) === column::TYPE_BOOLEAN) {
return format::boolean_as_text($value);
}
if ($this->get_user_field_type($fieldname) === column::TYPE_TIMESTAMP) {
return format::userdate($value, $row);
}
return s($value);
}
/**
* Returns a SQL statement to select all user fields necessary for fullname() function
*
* @param string $usertablealias
* @return string
*/
public static function get_name_fields_select(string $usertablealias = 'u'): string {
$userfields = array_map(static function(string $userfield) use ($usertablealias): string {
if (!empty($usertablealias)) {
$userfield = "{$usertablealias}.{$userfield}";
}
return $userfield;
}, fields::get_name_fields(true));
return implode(', ', $userfields);
}
/**
* User fields
*
* @return lang_string[]
*/
protected function get_user_fields(): array {
return [
'firstname' => new lang_string('firstname'),
'lastname' => new lang_string('lastname'),
'email' => new lang_string('email'),
'city' => new lang_string('city'),
'country' => new lang_string('country'),
'firstnamephonetic' => new lang_string('firstnamephonetic'),
'lastnamephonetic' => new lang_string('lastnamephonetic'),
'middlename' => new lang_string('middlename'),
'alternatename' => new lang_string('alternatename'),
'idnumber' => new lang_string('idnumber'),
'institution' => new lang_string('institution'),
'department' => new lang_string('department'),
'phone1' => new lang_string('phone1'),
'phone2' => new lang_string('phone2'),
'address' => new lang_string('address'),
'lastaccess' => new lang_string('lastaccess'),
'suspended' => new lang_string('suspended'),
'confirmed' => new lang_string('confirmed', 'admin'),
'username' => new lang_string('username'),
'moodlenetprofile' => new lang_string('moodlenetprofile', 'user'),
];
}
/**
* Return appropriate column type for given user field
*
* @param string $userfield
* @return int
*/
protected function get_user_field_type(string $userfield): int {
switch ($userfield) {
case 'confirmed':
case 'suspended':
$fieldtype = column::TYPE_BOOLEAN;
break;
case 'lastaccess':
$fieldtype = column::TYPE_TIMESTAMP;
break;
default:
$fieldtype = column::TYPE_TEXT;
break;
}
return $fieldtype;
}
/**
* Return list of all available filters
*
* @return filter[]
*/
protected function get_all_filters(): array {
global $DB;
$filters = [];
$tablealias = $this->get_table_alias('user');
// Fullname filter.
$canviewfullnames = has_capability('moodle/site:viewfullnames', context_system::instance());
[$fullnamesql, $fullnameparams] = fields::get_sql_fullname($tablealias, $canviewfullnames);
$filters[] = (new filter(
text::class,
'fullname',
new lang_string('fullname'),
$this->get_entity_name(),
$fullnamesql,
$fullnameparams
))
->add_joins($this->get_joins());
// User fields filters.
$fields = $this->get_user_fields();
foreach ($fields as $field => $name) {
$optionscallback = [static::class, 'get_options_for_' . $field];
if (is_callable($optionscallback)) {
$classname = select::class;
} else if ($this->get_user_field_type($field) === column::TYPE_BOOLEAN) {
$classname = boolean_select::class;
} else if ($this->get_user_field_type($field) === column::TYPE_TIMESTAMP) {
$classname = date::class;
} else {
$classname = text::class;
}
$filter = (new filter(
$classname,
$field,
$name,
$this->get_entity_name(),
$tablealias . '.' . $field
))
->add_joins($this->get_joins());
// Populate filter options by callback, if available.
if (is_callable($optionscallback)) {
$filter->set_options_callback($optionscallback);
}
$filters[] = $filter;
}
return $filters;
}
/**
* List of options for the field country.
*
* @return string[]
*/
public static function get_options_for_country(): array {
return array_map('shorten_text', get_string_manager()->get_list_of_countries());
}
}
@@ -0,0 +1,88 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\local\filters;
use MoodleQuickForm;
use core_reportbuilder\local\report\filter;
/**
* Base class for all report filters
*
* Filters provide a form for collecting user input, and then return appropriate SQL fragments based on these values
*
* @package core_reportbuilder
* @copyright 2021 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class base {
/** @var filter $filter */
protected $filter;
/** @var string $name */
protected $name;
/**
* Do not allow the constructor to be called directly or overridden
*
* @param filter $filter
*/
private function __construct(filter $filter) {
$this->filter = $filter;
$this->name = $filter->get_unique_identifier();
}
/**
* Creates an instance of a filter type, based on supplied report filter instance
*
* The report filter instance is used by reports/entities to define what should be filtered against, e.g. a SQL fragment
*
* @param filter $filter The report filter instance
* @return static
*/
final public static function create(filter $filter): self {
$filterclass = $filter->get_filter_class();
return new $filterclass($filter);
}
/**
* Returns the filter header
*
* @return string
*/
final public function get_header(): string {
return $this->filter->get_header();
}
/**
* Adds filter-specific form elements
*
* @param MoodleQuickForm $mform
*/
abstract public function setup_form(MoodleQuickForm $mform): void;
/**
* Returns the filter clauses to be used with SQL where
*
* @param array $values
* @return array [$sql, [...$params]]
*/
abstract public function get_sql_filter(array $values): array;
}
@@ -0,0 +1,104 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\local\filters;
use lang_string;
use MoodleQuickForm;
use core_reportbuilder\local\helpers\database;
/**
* Boolean report filter
*
* This filter accepts an expression that evaluates to 1 or 0, either a simple field such as "u.suspended", or a more complex
* expression such as "CASE WHEN <EXPRESSION> THEN 1 ELSE 0 END"
*
* @package core_reportbuilder
* @copyright 2021 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class boolean_select extends base {
/** @var int Any value */
public const ANY_VALUE = 0;
/** @var int Checked */
public const CHECKED = 1;
/** @var int Not checked */
public const NOT_CHECKED = 2;
/**
* Return an array of operators available for this filter
*
* @return lang_string[]
*/
private function get_operators(): array {
$operators = [
self::ANY_VALUE => new lang_string('filterisanyvalue', 'core_reportbuilder'),
self::CHECKED => new lang_string('yes'),
self::NOT_CHECKED => new lang_string('no'),
];
return $this->filter->restrict_limited_operators($operators);
}
/**
* Setup form
*
* @param MoodleQuickForm $mform
*/
public function setup_form(MoodleQuickForm $mform): void {
$operatorlabel = get_string('filterfieldoperator', 'core_reportbuilder', $this->get_header());
$mform->addElement('select', "{$this->name}_operator", $operatorlabel, $this->get_operators())
->setHiddenLabel(true);
$mform->setType("{$this->name}_operator", PARAM_INT);
$mform->setDefault("{$this->name}_operator", self::ANY_VALUE);
}
/**
* Return filter SQL
*
* @param array $values
* @return array
*/
public function get_sql_filter(array $values): array {
$fieldsql = $this->filter->get_field_sql();
$params = $this->filter->get_field_params();
$paramname = database::generate_param_name();
$operator = $values["{$this->name}_operator"] ?? self::ANY_VALUE;
switch ($operator) {
case self::CHECKED:
$fieldsql .= " = :{$paramname}";
$params[$paramname] = 1;
break;
case self::NOT_CHECKED:
$fieldsql .= " = :{$paramname}";
$params[$paramname] = 0;
break;
default:
// Invalid or inactive filter.
return ['', []];
}
return [$fieldsql, $params];
}
}
@@ -0,0 +1,68 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\local\filters;
use MoodleQuickForm;
use core_reportbuilder\local\helpers\database;
/**
* Course selector filter class implementation
*
* @package core_reportbuilder
* @copyright 2021 David Matamoros <[email protected]>.
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_selector extends base {
/**
* Setup form
*
* @param MoodleQuickForm $mform
*/
public function setup_form(MoodleQuickForm $mform): void {
$options = [
'multiple' => true,
];
$mform->addElement('course', $this->name . '_values', get_string('selectcourses', 'core_reportbuilder'), $options)
->setHiddenLabel(true);
}
/**
* Return filter SQL
*
* @param array $values
* @return array
*/
public function get_sql_filter(array $values): array {
global $DB;
$fieldsql = $this->filter->get_field_sql();
$params = $this->filter->get_field_params();
$courseids = $values["{$this->name}_values"] ?? [];
if (empty($courseids)) {
return ['', []];
}
$paramprefix = database::generate_param_name() . '_';
[$courseselect, $courseparams] = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED, $paramprefix);
return ["{$fieldsql} $courseselect", array_merge($params, $courseparams)];
}
}
@@ -0,0 +1,133 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\local\filters;
use lang_string;
use MoodleQuickForm;
use core_reportbuilder\local\helpers\database;
/**
* Date report filter
*
* This filter accepts a unix timestamp to perform date filtering on
*
* @package core_reportbuilder
* @copyright 2021 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class date extends base {
/** @var int Any value */
public const DATE_ANY = 0;
/** @var int Non-empty (positive) value */
public const DATE_NOT_EMPTY = 1;
/** @var int Empty (zero) value */
public const DATE_EMPTY = 2;
/** @var int Date within defined range */
public const DATE_RANGE = 3;
/**
* Return an array of operators available for this filter
*
* @return lang_string[]
*/
private function get_operators(): array {
$operators = [
self::DATE_ANY => new lang_string('filterisanyvalue', 'core_reportbuilder'),
self::DATE_NOT_EMPTY => new lang_string('filterisnotempty', 'core_reportbuilder'),
self::DATE_EMPTY => new lang_string('filterisempty', 'core_reportbuilder'),
self::DATE_RANGE => new lang_string('filterrange', 'core_reportbuilder'),
];
return $this->filter->restrict_limited_operators($operators);
}
/**
* Setup form
*
* @param MoodleQuickForm $mform
*/
public function setup_form(MoodleQuickForm $mform): void {
$operatorlabel = get_string('filterfieldoperator', 'core_reportbuilder', $this->get_header());
$mform->addElement('select', "{$this->name}_operator", $operatorlabel, $this->get_operators())->setHiddenLabel(true);
$mform->setType("{$this->name}_operator", PARAM_INT);
$mform->setDefault("{$this->name}_operator", self::DATE_ANY);
$mform->addElement('date_selector', "{$this->name}_from", get_string('filterdatefrom', 'core_reportbuilder'),
['optional' => true]);
$mform->setType("{$this->name}_from", PARAM_INT);
$mform->setDefault("{$this->name}_from", 0);
$mform->hideIf("{$this->name}_from", "{$this->name}_operator", 'neq', self::DATE_RANGE);
$mform->addElement('date_selector', "{$this->name}_to", get_string('filterdateto', 'core_reportbuilder'),
['optional' => true]);
$mform->setType("{$this->name}_to", PARAM_INT);
$mform->setDefault("{$this->name}_to", 0);
$mform->hideIf("{$this->name}_to", "{$this->name}_operator", 'neq', self::DATE_RANGE);
}
/**
* Return filter SQL
*
* @param array $values
* @return array
*/
public function get_sql_filter(array $values): array {
$fieldsql = $this->filter->get_field_sql();
$params = $this->filter->get_field_params();
$operator = $values["{$this->name}_operator"] ?? self::DATE_ANY;
switch ($operator) {
case self::DATE_NOT_EMPTY:
$sql = "{$fieldsql} IS NOT NULL AND {$fieldsql} <> 0";
break;
case self::DATE_EMPTY:
$sql = "{$fieldsql} IS NULL OR {$fieldsql} = 0";
break;
case self::DATE_RANGE:
$clauses = [];
$datefrom = (int)($values["{$this->name}_from"] ?? 0);
if ($datefrom > 0) {
$paramdatefrom = database::generate_param_name();
$clauses[] = "{$fieldsql} >= :{$paramdatefrom}";
$params[$paramdatefrom] = $datefrom;
}
$dateto = (int)($values["{$this->name}_to"] ?? 0);
if ($dateto > 0) {
$paramdateto = database::generate_param_name();
$clauses[] = "{$fieldsql} < :{$paramdateto}";
$params[$paramdateto] = $dateto;
}
$sql = implode(' AND ', $clauses);
break;
default:
// Invalid or inactive filter.
return ['', []];
}
return [$sql, $params];
}
}
@@ -0,0 +1,199 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\local\filters;
use core_reportbuilder\local\helpers\database;
/**
* Number report filter
*
* @package core_reportbuilder
* @copyright 2021 David Matamoros <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class number extends base {
/** @var int Any value */
public const ANY_VALUE = 0;
/** @var int Is not empty */
public const IS_NOT_EMPTY = 1;
/** @var int Is empty */
public const IS_EMPTY = 2;
/** @var int Less than */
public const LESS_THAN = 3;
/** @var int Greater than */
public const GREATER_THAN = 4;
/** @var int Equal to */
public const EQUAL_TO = 5;
/** @var int Equal or less than */
public const EQUAL_OR_LESS_THAN = 6;
/** @var int Equal or greater than */
public const EQUAL_OR_GREATER_THAN = 7;
/** @var int Range */
public const RANGE = 8;
/**
* Returns an array of comparison operators
*
* @return array of comparison operators
*/
private function get_operators(): array {
$operators = [
self::ANY_VALUE => get_string('filterisanyvalue', 'core_reportbuilder'),
self::IS_NOT_EMPTY => get_string('filterisnotempty', 'core_reportbuilder'),
self::IS_EMPTY => get_string('filterisempty', 'core_reportbuilder'),
self::LESS_THAN => get_string('filterlessthan', 'core_reportbuilder'),
self::GREATER_THAN => get_string('filtergreaterthan', 'core_reportbuilder'),
self::EQUAL_TO => get_string('filterisequalto', 'core_reportbuilder'),
self::EQUAL_OR_LESS_THAN => get_string('filterequalorlessthan', 'core_reportbuilder'),
self::EQUAL_OR_GREATER_THAN => get_string('filterequalorgreaterthan', 'core_reportbuilder'),
self::RANGE => get_string('filterrange', 'core_reportbuilder'),
];
return $this->filter->restrict_limited_operators($operators);
}
/**
* Adds controls specific to this filter in the form.
*
* @param \MoodleQuickForm $mform
*/
public function setup_form(\MoodleQuickForm $mform): void {
$objs = [];
$objs['select'] = $mform->createElement('select', $this->name . '_operator', null, $this->get_operators());
$mform->setType($this->name . '_operator', PARAM_INT);
$objs['text'] = $mform->createElement('text', $this->name . '_value1', null, ['size' => 3]);
$mform->setType($this->name . '_value1', PARAM_INT);
$mform->setDefault($this->name . '_value1', 0);
$objs['text2'] = $mform->createElement('text', $this->name . '_value2', null, ['size' => 3]);
$mform->setType($this->name . '_value2', PARAM_INT);
$mform->setDefault($this->name . '_value2', 0);
$mform->addElement('group', $this->name . '_grp', '', $objs, '', false);
$mform->hideIf($this->name . '_value1', $this->name . '_operator', 'eq', self::ANY_VALUE);
$mform->hideIf($this->name . '_value1', $this->name . '_operator', 'eq', self::IS_NOT_EMPTY);
$mform->hideIf($this->name . '_value1', $this->name . '_operator', 'eq', self::IS_EMPTY);
$mform->hideIf($this->name . '_value2', $this->name . '_operator', 'noteq', self::RANGE);
}
/**
* Return filter SQL
*
* @param array $values
* @return array array of two elements - SQL query and named parameters
*/
public function get_sql_filter(array $values) : array {
$operator = $values["{$this->name}_operator"] ?? self::ANY_VALUE;
$value1 = $values["{$this->name}_value1"] ?? null;
$value2 = $values["{$this->name}_value2"] ?? null;
// Validate filter form values.
if (!$this->validate_filter_values($operator, $value1, $value2)) {
// Filter configuration is invalid. Ignore the filter.
return ['', []];
}
$param = database::generate_param_name();
$param2 = database::generate_param_name();
$fieldsql = $this->filter->get_field_sql();
$params = $this->filter->get_field_params();
switch ($operator) {
case self::ANY_VALUE:
return ['', []];
case self::IS_NOT_EMPTY:
$res = "{$fieldsql} IS NOT NULL AND {$fieldsql} <> 0";
break;
case self::IS_EMPTY:
$res = "{$fieldsql} IS NULL OR {$fieldsql} = 0";
break;
case self::LESS_THAN:
$res = "{$fieldsql} < :{$param}";
$params[$param] = $value1;
break;
case self::GREATER_THAN:
$res = "{$fieldsql} > :{$param}";
$params[$param] = $value1;
break;
case self::EQUAL_TO:
$res = "{$fieldsql} = :{$param}";
$params[$param] = $value1;
break;
case self::EQUAL_OR_LESS_THAN:
$res = "{$fieldsql} <= :{$param}";
$params[$param] = $value1;
break;
case self::EQUAL_OR_GREATER_THAN:
$res = "{$fieldsql} >= :{$param}";
$params[$param] = $value1;
break;
case self::RANGE:
$res = "({$fieldsql} >= :{$param} AND {$fieldsql} <= :{$param2})";
$params[$param] = $value1;
$params[$param2] = $value2;
break;
default:
// Filter configuration is invalid. Ignore the filter.
return ['', []];
}
return [$res, $params];
}
/**
* Validate filter form values
*
* @param int $operator
* @param int|null $value1
* @param int|null $value2
* @return bool
*/
private function validate_filter_values(int $operator, ?int $value1, ?int $value2): bool {
// Check that for any of these operators value1 can not be null.
$requirescomparisonvalue = [
self::LESS_THAN,
self::GREATER_THAN,
self::EQUAL_TO,
self::EQUAL_OR_LESS_THAN,
self::EQUAL_OR_GREATER_THAN
];
if (in_array($operator, $requirescomparisonvalue) && $value1 === null) {
return false;
}
// When operator is between $value1 and $value2, can not be null.
if (($operator === self::RANGE) && ($value1 === null || $value2 === null)) {
return false;
}
return true;
}
}
@@ -0,0 +1,134 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\local\filters;
use MoodleQuickForm;
use core_reportbuilder\local\helpers\database;
/**
* Select report filter
*
* @package core_reportbuilder
* @copyright 2021 David Matamoros <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class select extends base {
/** @var int Any value */
public const ANY_VALUE = 0;
/** @var int Equal to */
public const EQUAL_TO = 1;
/** @var int Not equal to */
public const NOT_EQUAL_TO = 2;
/**
* Returns an array of comparison operators
*
* @return array
*/
private function get_operators(): array {
$operators = [
self::ANY_VALUE => get_string('filterisanyvalue', 'core_reportbuilder'),
self::EQUAL_TO => get_string('filterisequalto', 'core_reportbuilder'),
self::NOT_EQUAL_TO => get_string('filterisnotequalto', 'core_reportbuilder')
];
return $this->filter->restrict_limited_operators($operators);
}
/**
* Return the options for the filter as an array, to be used to populate the select input field. These options should be
* specified when creating the filter via the {@see set_options} or {@see set_options_callback} method
*
* @return array
*/
private function get_select_options(): array {
return (array) $this->filter->get_options();
}
/**
* Adds controls specific to this filter in the form.
*
* @param MoodleQuickForm $mform
*/
public function setup_form(MoodleQuickForm $mform): void {
$elements = [];
$elements['operator'] = $mform->createElement('select', $this->name . '_operator', null, $this->get_operators());
// If a multi-dimensional array is passed, we need to use a different element type.
$options = $this->get_select_options();
$element = (count($options) == count($options, COUNT_RECURSIVE) ? 'select' : 'selectgroups');
$elements['value'] = $mform->createElement($element, $this->name . '_value', null, $options);
$mform->addElement('group', $this->name . '_group', '', $elements, '', false);
$mform->hideIf($this->name . '_value', $this->name . '_operator', 'eq', self::ANY_VALUE);
}
/**
* Return filter SQL
*
* Note that operators must be of type integer, while values can be integer or string.
*
* @param array $values
* @return array array of two elements - SQL query and named parameters
*/
public function get_sql_filter(array $values): array {
$name = database::generate_param_name();
$operator = $values["{$this->name}_operator"] ?? self::ANY_VALUE;
$value = $values["{$this->name}_value"] ?? 0;
$fieldsql = $this->filter->get_field_sql();
$params = $this->filter->get_field_params();
// Validate filter form values.
if (!$this->validate_filter_values((int) $operator, $value)) {
// Filter configuration is invalid. Ignore the filter.
return ['', []];
}
switch ($operator) {
case self::EQUAL_TO:
$fieldsql .= "=:$name";
$params[$name] = $value;
break;
case self::NOT_EQUAL_TO:
$fieldsql .= "<>:$name";
$params[$name] = $value;
break;
default:
return ['', []];
}
return [$fieldsql, $params];
}
/**
* Validate filter form values
*
* @param int|null $operator
* @param mixed|null $value
* @return bool
*/
private function validate_filter_values(?int $operator, $value): bool {
return !($operator === null || $value === '');
}
}
@@ -0,0 +1,193 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\local\filters;
use core_reportbuilder\local\helpers\database;
/**
* Text report filter
*
* @package core_reportbuilder
* @copyright 2021 David Matamoros <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class text extends base {
/** @var int */
public const ANY_VALUE = 0;
/** @var int */
public const CONTAINS = 1;
/** @var int */
public const DOES_NOT_CONTAIN = 2;
/** @var int */
public const IS_EQUAL_TO = 3;
/** @var int */
public const IS_NOT_EQUAL_TO = 4;
/** @var int */
public const STARTS_WITH = 5;
/** @var int */
public const ENDS_WITH = 6;
/** @var int */
public const IS_EMPTY = 7;
/** @var int */
public const IS_NOT_EMPTY = 8;
/**
* Return an array of operators available for this filter
*
* @return array of comparison operators
*/
private function get_operators() : array {
$operators = [
self::ANY_VALUE => get_string('filterisanyvalue', 'core_reportbuilder'),
self::CONTAINS => get_string('filtercontains', 'core_reportbuilder'),
self::DOES_NOT_CONTAIN => get_string('filterdoesnotcontain', 'core_reportbuilder'),
self::IS_EQUAL_TO => get_string('filterisequalto', 'core_reportbuilder'),
self::IS_NOT_EQUAL_TO => get_string('filterisnotequalto', 'core_reportbuilder'),
self::STARTS_WITH => get_string('filterstartswith', 'core_reportbuilder'),
self::ENDS_WITH => get_string('filterendswith', 'core_reportbuilder'),
self::IS_EMPTY => get_string('filterisempty', 'core_reportbuilder'),
self::IS_NOT_EMPTY => get_string('filterisnotempty', 'core_reportbuilder')
];
return $this->filter->restrict_limited_operators($operators);
}
/**
* Adds controls specific to this filter in the form.
*
* Operator selector use the "$this->name . '_operator'" naming convention and the fields to enter custom values should
* use "$this->name . '_value'" or _value1/_value2/... in case there is more than one field for their naming.
*
* @param \MoodleQuickForm $mform
*/
public function setup_form(\MoodleQuickForm $mform): void {
$elements = [];
$elements['operator'] = $mform->createElement('select', $this->name . '_operator',
get_string('filterfieldoperator', 'core_reportbuilder', $this->get_header()), $this->get_operators());
$elements['value'] = $mform->createElement('text', $this->name . '_value',
get_string('filterfieldvalue', 'core_reportbuilder', $this->get_header()));
$mform->addElement('group', $this->name . '_group', '', $elements, '', false);
$mform->setType($this->name . '_value', PARAM_RAW);
$mform->hideIf($this->name . '_value', $this->name . '_operator', 'eq', self::ANY_VALUE);
$mform->hideIf($this->name . '_value', $this->name . '_operator', 'eq', self::IS_EMPTY);
$mform->hideIf($this->name . '_value', $this->name . '_operator', 'eq', self::IS_NOT_EMPTY);
}
/**
* Return filter SQL
*
* @param array|null $values
* @return array array of two elements - SQL query and named parameters
*/
public function get_sql_filter(?array $values) : array {
global $DB;
$name = database::generate_param_name();
if (!$values) {
return ['', []];
}
$operator = (int) ($values["{$this->name}_operator"] ?? self::ANY_VALUE);
$value = $values["{$this->name}_value"] ?? '';
$fieldsql = $this->filter->get_field_sql();
$params = $this->filter->get_field_params();
// Validate filter form values.
if (!$this->validate_filter_values($operator, $value)) {
// Filter configuration is invalid. Ignore the filter.
return ['', []];
}
switch($operator) {
case self::CONTAINS:
$res = $DB->sql_like($fieldsql, ":$name", false, false);
$value = $DB->sql_like_escape($value);
$params[$name] = "%$value%";
break;
case self::DOES_NOT_CONTAIN:
$res = $DB->sql_like($fieldsql, ":$name", false, false, true);
$value = $DB->sql_like_escape($value);
$params[$name] = "%$value%";
break;
case self::IS_EQUAL_TO:
$res = $DB->sql_equal($fieldsql, ":$name", false, false);
$params[$name] = $value;
break;
case self::IS_NOT_EQUAL_TO:
$res = $DB->sql_equal($fieldsql, ":$name", false, false, true);
$params[$name] = $value;
break;
case self::STARTS_WITH:
$res = $DB->sql_like($fieldsql, ":$name", false, false);
$value = $DB->sql_like_escape($value);
$params[$name] = "$value%";
break;
case self::ENDS_WITH:
$res = $DB->sql_like($fieldsql, ":$name", false, false);
$value = $DB->sql_like_escape($value);
$params[$name] = "%$value";
break;
case self::IS_EMPTY: // Note we also account for field not existing here.
$res = "COALESCE({$fieldsql}, '') = :{$name}";
$params[$name] = '';
break;
case self::IS_NOT_EMPTY:
$res = "COALESCE({$fieldsql}, '') != :{$name}";
$params[$name] = '';
break;
default:
// Filter configuration is invalid. Ignore the filter.
return ['', []];
}
return array($res, $params);
}
/**
* Validate filter form values
*
* @param int $operator
* @param string|null $value
* @return bool
*/
private function validate_filter_values(int $operator, ?string $value): bool {
$operatorsthatdontrequirevalue = [
self::ANY_VALUE,
self::IS_EMPTY,
self::IS_NOT_EMPTY,
];
if ($value === '' && !in_array($operator, $operatorsthatdontrequirevalue)) {
return false;
}
return true;
}
}
@@ -0,0 +1,296 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\local\helpers;
use core_reportbuilder\local\filters\boolean_select;
use core_reportbuilder\local\filters\date;
use core_reportbuilder\local\filters\number;
use core_reportbuilder\local\filters\select;
use core_reportbuilder\local\filters\text;
use core_reportbuilder\local\report\column;
use core_reportbuilder\local\report\filter;
use lang_string;
use stdClass;
use core_customfield\data;
use core_customfield\data_controller;
use core_customfield\field_controller;
use core_customfield\handler;
/**
* Helper class for course custom fields.
*
* @package core_reportbuilder
* @copyright 2021 Sara Arjona <[email protected]> based on David Matamoros <[email protected]> code.
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class custom_fields {
/** @var string $entityname Name of the entity */
private $entityname;
/** @var handler $handler The handler for the customfields */
private $handler;
/** @var int $tablefieldalias The table alias and the field name (table.field) that matches the customfield instanceid. */
private $tablefieldalias;
/** @var array additional joins */
private $joins = [];
/**
* Class customfields constructor.
*
* @param string $tablefieldalias table alias and the field name (table.field) that matches the customfield instanceid.
* @param string $entityname name of the entity in the report where we add custom fields.
* @param string $component component name of full frankenstyle plugin name.
* @param string $area name of the area (each component/plugin may define handlers for multiple areas).
* @param int $itemid item id if the area uses them (usually not used).
*/
public function __construct(string $tablefieldalias, string $entityname, string $component, string $area, int $itemid = 0) {
$this->tablefieldalias = $tablefieldalias;
$this->entityname = $entityname;
$this->handler = handler::get_handler($component, $area, $itemid);
}
/**
* Additional join that is needed.
*
* @param string $join
* @return self
*/
public function add_join(string $join): self {
$this->joins[trim($join)] = trim($join);
return $this;
}
/**
* Additional joins that are needed.
*
* @param array $joins
* @return self
*/
public function add_joins(array $joins): self {
foreach ($joins as $join) {
$this->add_join($join);
}
return $this;
}
/**
* Return joins
*
* @return string[]
*/
private function get_joins(): array {
return array_values($this->joins);
}
/**
* Gets the custom fields columns for the report.
*
* Column will be named as 'customfield_' + customfield shortname.
*
* @return column[]
*/
public function get_columns(): array {
$columns = [];
$categorieswithfields = $this->handler->get_categories_with_fields();
foreach ($categorieswithfields as $fieldcategory) {
$categoryfields = $fieldcategory->get_fields();
foreach ($categoryfields as $field) {
$customdatatablealias = database::generate_alias();
$datacontroller = data_controller::create(0, null, $field);
$datafield = $datacontroller->datafield();
$alias = 'customfield_' . $field->get('shortname');
$selectfields = "{$customdatatablealias}.{$datafield} as {$alias}, {$customdatatablealias}.id";
if ($datafield === 'value') {
// We will take the format into account when displaying the individual values.
$selectfields .= ", {$customdatatablealias}.valueformat";
}
$columnname = $field->get_formatted_name();
$columntype = $this->get_column_type($field, $datafield);
$newcolumn = (new column(
'customfield_' . $field->get('shortname'),
new lang_string('customfieldcolumn', 'core_reportbuilder', $columnname),
$this->entityname
))
->add_joins($this->get_joins())
->add_join("LEFT JOIN {customfield_data} {$customdatatablealias} " .
"ON {$customdatatablealias}.fieldid = " . $field->get('id') . " " .
"AND {$customdatatablealias}.instanceid = {$this->tablefieldalias}")
->add_fields($selectfields)
->set_type($columntype)
->set_is_sortable($columntype !== column::TYPE_LONGTEXT)
->add_callback([$this, 'customfield_value'], $field)
// Important. If the handler implements can_view() function, it will be called with parameter $instanceid=0.
// This means that per-instance access validation will be ignored.
->set_is_available($this->handler->can_view($field, 0));
$columns[] = $newcolumn;
}
}
return $columns;
}
/**
* Returns the column type
*
* @param field_controller $field
* @param string $datafield
* @return int
*/
private function get_column_type(field_controller $field, string $datafield): int {
if ($field->get('type') === 'checkbox') {
return column::TYPE_BOOLEAN;
}
if ($field->get('type') === 'date') {
return column::TYPE_TIMESTAMP;
}
if ($datafield === 'intvalue') {
return column::TYPE_INTEGER;
}
if ($datafield === 'decvalue') {
return column::TYPE_FLOAT;
}
if ($datafield === 'value') {
return column::TYPE_LONGTEXT;
}
return column::TYPE_TEXT;
}
/**
* Returns all available filters on custom fields.
*
* Filter will be named as 'customfield_' + customfield shortname.
*
* @return filter[]
*/
public function get_filters(): array {
$filters = [];
$categorieswithfields = $this->handler->get_categories_with_fields();
foreach ($categorieswithfields as $fieldcategory) {
$categoryfields = $fieldcategory->get_fields();
foreach ($categoryfields as $field) {
$customdatatablealias = database::generate_alias();
$datacontroller = data_controller::create(0, null, $field);
$datafield = $datacontroller->datafield();
$typeclass = $this->get_filter_class_type($datacontroller);
$filter = (new filter(
$typeclass,
'customfield_' . $field->get('shortname'),
new lang_string('customfieldcolumn', 'core_reportbuilder', $field->get('name')),
$this->entityname,
"{$customdatatablealias}.{$datafield}"
))
->add_joins($this->get_joins())
->add_join("LEFT JOIN {customfield_data} {$customdatatablealias} " .
"ON {$customdatatablealias}.fieldid = " . $field->get('id') . " " .
"AND {$customdatatablealias}.instanceid = {$this->tablefieldalias}");
// Options are stored inside configdata json string and we need to convert it to array.
if ($field->get('type') === 'select') {
$filter->set_options_callback(static function() use ($field): array {
$options = explode("\r\n", $field->get_configdata_property('options'));
// Method set_options starts using array at index 1. we shift one position on this array.
// In course settings this menu has an empty option and we need to respect that.
array_unshift($options, " ");
unset($options[0]);
return $options;
});
}
$filters[] = $filter;
}
}
return $filters;
}
/**
* Returns class for the filter element that should be used for the field
*
* In some situation we can assume what kind of data is stored in the customfield plugin and we can
* display appropriate filter form element. For all others assume text filter.
*
* @param data_controller $datacontroller
* @return string
*/
private function get_filter_class_type(data_controller $datacontroller): string {
$type = $datacontroller->get_field()->get('type');
switch ($type) {
case 'checkbox':
$classtype = boolean_select::class;
break;
case 'date':
$classtype = date::class;
break;
case 'select':
$classtype = select::class;
break;
default:
// To support third party field type we need to account for stored numbers.
$datafield = $datacontroller->datafield();
if ($datafield === 'intvalue' || $datafield === 'decvalue') {
$classtype = number::class;
} else {
$classtype = text::class;
}
break;
}
return $classtype;
}
/**
* Format for custom fields value. We get the correct custom field value using export_value method.
*
* @param mixed $value Current value.
* @param stdClass $row Full row.
* @param field_controller $field Field controller object.
* @return mixed|null
*/
public function customfield_value($value, stdClass $row, field_controller $field) {
$defaults = [
'id' => -1,
'shortcharvalue' => $value,
'charvalue' => $value,
'intvalue' => (int)$value,
'decvalue' => (float)$value,
'value' => $value,
'fieldid' => $field->get('id'),
'valueformat' => FORMAT_HTML,
];
$row = array_intersect_key(array_merge($defaults, (array)$row), data::properties_definition());
$data = data_controller::create(0, (object)$row, $field);
return $data->export_value();
}
}
@@ -0,0 +1,77 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\local\helpers;
use coding_exception;
/**
* Helper functions for DB manipulations
*
* @package core_reportbuilder
* @copyright 2019 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class database {
/** @var string Prefix for generated aliases */
private const GENERATE_ALIAS_PREFIX = 'rbalias';
/** @var string Prefix for generated param names names */
private const GENERATE_PARAM_PREFIX = 'rbparam';
/**
* Generates unique table/column alias that must be used in generated SQL
*
* @return string
*/
public static function generate_alias(): string {
static $aliascount = 0;
return static::GENERATE_ALIAS_PREFIX . ($aliascount++);
}
/**
* Generates unique parameter name that must be used in generated SQL
*
* @return string
*/
public static function generate_param_name(): string {
static $paramcount = 0;
return static::GENERATE_PARAM_PREFIX . ($paramcount++);
}
/**
* Validate that parameter names were generated using {@see generate_param_name}.
*
* @param array $params
* @return bool
* @throws coding_exception For invalid params.
*/
public static function validate_params(array $params): bool {
$nonmatchingkeys = array_filter($params, static function($key): bool {
return !preg_match('/^' . static::GENERATE_PARAM_PREFIX . '[\d]+/', $key);
}, ARRAY_FILTER_USE_KEY);
if (!empty($nonmatchingkeys)) {
throw new coding_exception('Invalid parameter names', implode(', ', array_keys($nonmatchingkeys)));
}
return true;
}
}
@@ -0,0 +1,53 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\local\helpers;
use stdClass;
/**
* Class containing helper methods for format columns data as callbacks.
*
* @package core_reportbuilder
* @copyright 2021 Sara Arjona <[email protected]> based on Alberto Lara Hernández <[email protected]> code.
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class format {
/**
* Returns formatted date.
*
* @param int $value Unix timestamp
* @param stdClass $row
* @param string|null $format Format string for strftime
* @return string
*/
public static function userdate(int $value, stdClass $row, ?string $format = null): string {
return $value ? userdate($value, $format) : '';
}
/**
* Returns yes/no string depending on the given value
*
* @param bool $value
* @return string
*/
public static function boolean_as_text(bool $value): string {
return $value ? get_string('yes') : get_string('no');
}
}
@@ -0,0 +1,175 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\local\helpers;
use core_text;
/**
* This class handles the setting and retrieving of a users' filter values for given reports
*
* It is currently using the user preference API as a storage mechanism
*
* @package core_reportbuilder
* @copyright 2021 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class user_filter_manager {
/** @var int The size of each chunk, matching the maximum length of a single user preference */
private const PREFERENCE_CHUNK_SIZE = 1333;
/** @var string The prefix used to name the stored user preferences */
private const PREFERENCE_NAME_PREFIX = 'reportbuilder-report-';
/**
* Generate user preference name for given report
*
* @param int $reportid
* @param int $index
* @return string
*/
private static function user_preference_name(int $reportid, int $index): string {
return static::PREFERENCE_NAME_PREFIX . "{$reportid}-{$index}";
}
/**
* Set user filters for given report
*
* @param int $reportid
* @param array $values
* @param int|null $userid
* @return bool
*/
public static function set(int $reportid, array $values, int $userid = null): bool {
$jsonvalues = json_encode($values);
$jsonchunks = str_split($jsonvalues, static::PREFERENCE_CHUNK_SIZE);
foreach ($jsonchunks as $index => $jsonchunk) {
$userpreference = static::user_preference_name($reportid, $index);
set_user_preference($userpreference, $jsonchunk, $userid);
}
// Ensure any subsequent preferences are reset (to account for number of chunks decreasing).
static::reset_all($reportid, $userid, $index + 1);
return true;
}
/**
* Get user filters for given report
*
* @param int $reportid
* @param int|null $userid
* @return array
*/
public static function get(int $reportid, int $userid = null): array {
$jsonvalues = '';
$index = 0;
// We'll repeatedly append chunks to our JSON string, until we hit one that is below the maximum length.
do {
$userpreference = static::user_preference_name($reportid, $index++);
$jsonchunk = get_user_preferences($userpreference, '', $userid);
$jsonvalues .= $jsonchunk;
} while (core_text::strlen($jsonchunk) === static::PREFERENCE_CHUNK_SIZE);
return (array) json_decode($jsonvalues);
}
/**
* Merge individual user filter values for given report
*
* @param int $reportid
* @param array $values
* @param int|null $userid
* @return bool
*/
public static function merge(int $reportid, array $values, int $userid = null): bool {
$existing = static::get($reportid, $userid);
return static::set($reportid, array_merge($existing, $values), $userid);
}
/**
* Reset all user filters for given report
*
* @param int $reportid
* @param int|null $userid
* @param int $index If specified, then preferences will be reset starting from this index
* @return bool
*/
public static function reset_all(int $reportid, int $userid = null, int $index = 0): bool {
// We'll repeatedly retrieve and reset preferences, until we hit one that is below the maximum length.
do {
$userpreference = static::user_preference_name($reportid, $index++);
$jsonchunk = get_user_preferences($userpreference, '', $userid);
unset_user_preference($userpreference, $userid);
} while (core_text::strlen($jsonchunk) === static::PREFERENCE_CHUNK_SIZE);
return true;
}
/**
* Reset single user filter for given report
*
* @param int $reportid
* @param string $uniqueidentifier
* @param int|null $userid
* @return bool
*/
public static function reset_single(int $reportid, string $uniqueidentifier, int $userid = null): bool {
$originalvalues = static::get($reportid, $userid);
// Remove any filters whose name is prefixed by given identifier.
$values = array_filter($originalvalues, static function(string $filterkey) use ($uniqueidentifier): bool {
return core_text::strpos($filterkey, $uniqueidentifier) !== 0;
}, ARRAY_FILTER_USE_KEY);
return static::set($reportid, $values, $userid);
}
/**
* Get all report filters for given user
*
* This is primarily designed for the privacy provider, and allows us to preserve all the preference logic within this class.
*
* @param int $userid
* @return array
*/
public static function get_all_for_user(int $userid): array {
global $DB;
$prefs = [];
// We need to locate the first preference chunk of all report filters.
$select = 'userid = :userid AND ' . $DB->sql_like('name', ':namelike');
$params = [
'userid' => $userid,
'namelike' => $DB->sql_like_escape(static::PREFERENCE_NAME_PREFIX) . '%-0',
];
$preferences = $DB->get_fieldset_select('user_preferences', 'name', $select, $params);
// Retrieve all found filters.
foreach ($preferences as $preference) {
preg_match('/^' . static::PREFERENCE_NAME_PREFIX . '(?<reportid>\d+)\-/', $preference, $matches);
$prefs[static::PREFERENCE_NAME_PREFIX . $matches['reportid']] = static::get((int) $matches['reportid'], $userid);
}
return $prefs;
}
}
@@ -0,0 +1,242 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\local\helpers;
use context_system;
use core_reportbuilder\local\filters\boolean_select;
use core_reportbuilder\local\filters\date;
use core_reportbuilder\local\filters\select;
use core_reportbuilder\local\filters\text;
use core_reportbuilder\local\report\column;
use core_reportbuilder\local\report\filter;
use lang_string;
use profile_field_base;
use stdClass;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot.'/user/profile/lib.php');
/**
* Helper class for user profile fields.
*
* @package core_reportbuilder
* @copyright 2021 David Matamoros <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class user_profile_fields {
/** @var array user profile fields */
private $userprofilefields;
/** @var string $entityname Name of the entity */
private $entityname;
/** @var int $usertablefieldalias The user table/field alias */
private $usertablefieldalias;
/** @var array additional joins */
private $joins = [];
/**
* Class userprofilefields constructor.
*
* @param string $usertablefieldalias The user table/field alias used when adding columns and filters.
* @param string $entityname The entity name used when adding columns and filters.
*/
public function __construct(string $usertablefieldalias, string $entityname) {
$this->usertablefieldalias = $usertablefieldalias;
$this->entityname = $entityname;
$this->userprofilefields = $this->get_user_profile_fields();
}
/**
* Retrieves the list of available/visible user profile fields
*
* @return profile_field_base[]
*/
private function get_user_profile_fields(): array {
return array_filter(profile_get_user_fields_with_data(0), static function($profilefield): bool {
return (int)$profilefield->field->visible === (int)PROFILE_VISIBLE_ALL;
});
}
/**
* Additional join that is needed.
*
* @param string $join
* @return self
*/
public function add_join(string $join): self {
$this->joins[trim($join)] = trim($join);
return $this;
}
/**
* Additional joins that are needed.
*
* @param array $joins
* @return self
*/
public function add_joins(array $joins): self {
foreach ($joins as $join) {
$this->add_join($join);
}
return $this;
}
/**
* Return joins
*
* @return string[]
*/
private function get_joins(): array {
return array_values($this->joins);
}
/**
* Return the user profile fields visible columns.
*
* @return column[]
*/
public function get_columns(): array {
$columns = [];
foreach ($this->userprofilefields as $profilefield) {
$userinfotablealias = database::generate_alias();
$column = (new column(
'profilefield_' . $profilefield->field->shortname,
new lang_string('customfieldcolumn', 'core_reportbuilder',
format_string($profilefield->field->name, true,
['escape' => true, 'context' => context_system::instance()])),
$this->entityname
))
->add_joins($this->get_joins())
->add_join("LEFT JOIN {user_info_data} {$userinfotablealias} " .
"ON {$userinfotablealias}.userid = {$this->usertablefieldalias} " .
"AND {$userinfotablealias}.fieldid = {$profilefield->fieldid}")
->add_field("{$userinfotablealias}.data", 'profilefield_' . $profilefield->field->shortname)
->set_type($this->get_user_field_type($profilefield->field->datatype))
->add_callback([$this, 'format_profile_field'], $profilefield);
$columns[] = $column;
}
return $columns;
}
/**
* Get custom user profile fields filters.
*
* @return filter[]
*/
public function get_filters(): array {
global $DB;
$filters = [];
foreach ($this->userprofilefields as $profilefield) {
$userinfotablealias = database::generate_alias();
$field = "{$userinfotablealias}.data";
switch ($profilefield->field->datatype) {
case 'checkbox':
$classname = boolean_select::class;
$field = $DB->sql_cast_char2int($field);
break;
case 'datetime':
$classname = date::class;
$field = $DB->sql_cast_char2int($field);
break;
case 'menu':
$classname = select::class;
break;
case 'text':
case 'textarea':
default:
$field = $DB->sql_compare_text($field, 255);
$classname = text::class;
break;
}
$filter = (new filter(
$classname,
'profilefield_' . $profilefield->field->shortname,
new lang_string('customfieldcolumn', 'core_reportbuilder',
format_string($profilefield->field->name, true,
['escape' => false, 'context' => context_system::instance()])),
$this->entityname,
$field
))
->add_joins($this->get_joins())
->add_join("LEFT JOIN {user_info_data} {$userinfotablealias} " .
"ON {$userinfotablealias}.userid = {$this->usertablefieldalias} " .
"AND {$userinfotablealias}.fieldid = {$profilefield->fieldid}");
// If menu type then set filter options as appropriate.
if ($profilefield->field->datatype === 'menu') {
$filter->set_options($profilefield->options);
}
$filters[] = $filter;
}
return $filters;
}
/**
* Get user profile field type for report.
*
* @param string $userfield user field.
* @return int the constant equivalent to this custom field type.
*/
protected function get_user_field_type(string $userfield): int {
switch ($userfield) {
case 'checkbox':
$customfieldtype = column::TYPE_BOOLEAN;
break;
case 'datetime':
$customfieldtype = column::TYPE_TIMESTAMP;
break;
case 'textarea':
$customfieldtype = column::TYPE_LONGTEXT;
break;
case 'menu':
case 'text':
default:
$customfieldtype = column::TYPE_TEXT;
break;
}
return $customfieldtype;
}
/**
* Formatter for a profile field. It formats the field according to its type.
*
* @param mixed $value
* @param stdClass $row
* @param profile_field_base $field
* @return string
*/
public static function format_profile_field($value, stdClass $row, profile_field_base $field): string {
$field->data = $value;
return $field->display_data();
}
}
@@ -0,0 +1,101 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\local\models;
use context;
use context_system;
use core\persistent;
use core_reportbuilder\local\report\base;
/**
* Persistent class to represent a report
*
* @package core_reportbuilder
* @copyright 2018 Alberto Lara Hernández <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class report extends persistent {
/** @var string The table name. */
public const TABLE = 'reportbuilder_report';
/**
* Return the definition of the properties of this model
*
* @return array
*/
protected static function define_properties(): array {
return [
'source' => [
'type' => PARAM_RAW,
],
'type' => [
'type' => PARAM_INT,
'choices' => [
base::TYPE_CUSTOM_REPORT,
base::TYPE_SYSTEM_REPORT,
],
],
'contextid' => [
'type' => PARAM_INT,
'default' => static function(): int {
return context_system::instance()->id;
}
],
'component' => [
'type' => PARAM_COMPONENT,
'default' => '',
],
'area' => [
'type' => PARAM_AREA,
'default' => '',
],
'itemid' => [
'type' => PARAM_INT,
'default' => 0,
],
'usercreated' => [
'type' => PARAM_INT,
'default' => static function(): int {
global $USER;
return (int) $USER->id;
},
],
];
}
/**
* Return report ID
*
* @return int
*/
protected function get_id(): int {
return (int) $this->raw_get('id');
}
/**
* Return report context, used by exporters
*
* @return context
*/
public function get_context(): context {
return context::instance_by_id($this->raw_get('contextid'));
}
}
@@ -0,0 +1,131 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\local\report;
use moodle_url;
use pix_icon;
use popup_action;
use stdClass;
/**
* Class to represent a report action
*
* @package core_reportbuilder
* @copyright 2021 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
final class action {
/** @var moodle_url $url */
protected $url;
/** @var pix_icon $icon */
protected $icon;
/** @var array $attributes */
protected $attributes;
/** @var bool $popup */
protected $popup;
/** @var callable[] $callbacks */
protected $callbacks = [];
/**
* Create an instance of an action to be added to a report. Both the parameters of the URL, and the attributes parameter
* support placeholders which will be replaced with appropriate row values, e.g.:
*
* new action(new moodle_url('/', ['id' => ':id']), new pix_icon(...), ['data-id' => ':id'])
*
* Note that all expected placeholders should be added as base fields to the report
*
* @param moodle_url $url
* @param pix_icon $icon
* @param array $attributes
* @param bool $popup
*/
public function __construct(
moodle_url $url,
pix_icon $icon,
array $attributes = [],
bool $popup = false
) {
$this->url = $url;
$this->icon = $icon;
$this->attributes = $attributes;
$this->popup = $popup;
}
/**
* Adds callback to the action. Used to verify action is available to current user, or preprocess values used in placeholders
*
* Multiple callbacks can be added. If at least one returns false then the action will not be displayed
*
* @param callable $callback
* @return self
*/
public function add_callback(callable $callback): self {
$this->callbacks[] = $callback;
return $this;
}
/**
* Return renderer action icon suitable for output
*
* @uses core_renderer::action_icon()
*
* @param stdClass $row
* @return string|null
*/
public function get_action_link(stdClass $row): ?string {
global $OUTPUT;
foreach ($this->callbacks as $callback) {
$row = clone $row; // Clone so we don't modify the shared row inside a callback.
if (!$callback($row)) {
return null;
}
}
// Create a new moodle_url instance with our filled in placeholders for this row.
$url = new moodle_url(
$this->url->out_omit_querystring(true),
self::replace_placeholders($this->url->params(), $row)
);
$popupaction = $this->popup ? new popup_action('click', $url) : null;
return $OUTPUT->action_icon($url, $this->icon, $popupaction, self::replace_placeholders($this->attributes, $row));
}
/**
* Given an array of values, replace all placeholders with corresponding property of the given row
*
* @param array $values
* @param stdClass $row
* @return array
*/
private static function replace_placeholders(array $values, stdClass $row): array {
return array_map(static function($value) use ($row) {
return preg_replace_callback('/^:(?<property>.*)$/', static function(array $matches) use ($row): string {
return (string) ($row->{$matches['property']} ?? '');
}, $value);
}, $values);
}
}
+482
View File
@@ -0,0 +1,482 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\local\report;
use coding_exception;
use context;
use lang_string;
use core_reportbuilder\local\entities\base as entity_base;
use core_reportbuilder\local\filters\base as filter_base;
use core_reportbuilder\local\helpers\database;
use core_reportbuilder\local\helpers\user_filter_manager;
use core_reportbuilder\local\models\report;
/**
* Base class for all reports
*
* @package core_reportbuilder
* @copyright 2020 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class base {
/** @var int Custom report type value */
public const TYPE_CUSTOM_REPORT = 0;
/** @var int System report type value */
public const TYPE_SYSTEM_REPORT = 1;
/** @var int Default paging limit */
public const DEFAULT_PAGESIZE = 30;
/** @var report $report Report persistent */
private $report;
/** @var string $maintable */
private $maintable = '';
/** @var string $maintablealias */
private $maintablealias = '';
/** @var array $sqljoins */
private $sqljoins = [];
/** @var array $sqlwheres */
private $sqlwheres = [];
/** @var array $sqlparams */
private $sqlparams = [];
/** @var entity_base[] $entities */
private $entities = [];
/** @var lang_string[] */
private $entitytitles = [];
/** @var column[] $columns */
private $columns = [];
/** @var filter[] $filters */
private $filters = [];
/** @var bool $downloadable Set if the report can be downloaded */
private $downloadable = false;
/** @var string $downloadfilename Name of the downloaded file */
private $downloadfilename = '';
/**
* Base report constructor
*
* @param report $report
*/
public function __construct(report $report) {
$this->report = $report;
// Initialise and validate the report.
$this->initialise();
$this->validate();
}
/**
* Returns persistent class used when initialising this report
*
* @return report
*/
final public function get_report_persistent(): report {
return $this->report;
}
/**
* Initialise report. Specify which columns, filters, etc should be present
*
* To set the base query use:
* - {@see set_main_table}
* - {@see add_base_condition_simple} or {@see add_base_condition_sql}
* - {@see add_join}
*
* To add content to the report use:
* - {@see add_entity}
* - {@see add_column}
* - {@see add_filter}
* - etc
*/
abstract protected function initialise(): void;
/**
* Output the report
*
* @return string
*/
abstract public function output(): string;
/**
* Get the report availability. Sub-classes should override this method to declare themselves unavailable, for example if
* they require classes that aren't present due to missing plugin
*
* @return bool
*/
public static function is_available(): bool {
return true;
}
/**
* Perform some basic validation about expected class properties
*
* @throws coding_exception
*/
protected function validate(): void {
if (empty($this->maintable)) {
throw new coding_exception('Report must define main table by calling $this->set_main_table()');
}
if (empty($this->columns)) {
throw new coding_exception('Report must define at least one column by calling $this->add_column()');
}
}
/**
* Set the main table and alias for the SQL query
*
* @param string $tablename
* @param string $tablealias
*/
final public function set_main_table(string $tablename, string $tablealias = ''): void {
$this->maintable = $tablename;
$this->maintablealias = $tablealias;
}
/**
* Get the main table name
*
* @return string
*/
final public function get_main_table(): string {
return $this->maintable;
}
/**
* Get the alias for the main table
*
* @return string
*/
final public function get_main_table_alias(): string {
return $this->maintablealias;
}
/**
* Adds report JOIN clause that is always added
*
* @param string $join
* @param array $params
* @param bool $validateparams Some queries might add non-standard params and validation could fail
*/
protected function add_join(string $join, array $params = [], bool $validateparams = true): void {
if ($validateparams) {
database::validate_params($params);
}
$this->sqljoins[trim($join)] = trim($join);
$this->sqlparams += $params;
}
/**
* Return report JOIN clauses
*
* @return array
*/
public function get_joins(): array {
return array_values($this->sqljoins);
}
/**
* Define simple "field = value" clause to apply to the report query
*
* @param string $fieldname
* @param mixed $fieldvalue
*/
final public function add_base_condition_simple(string $fieldname, $fieldvalue): void {
if ($fieldvalue === null) {
$this->add_base_condition_sql("{$fieldname} IS NULL");
} else {
$fieldvalueparam = database::generate_param_name();
$this->add_base_condition_sql("{$fieldname} = :{$fieldvalueparam}", [
$fieldvalueparam => $fieldvalue,
]);
}
}
/**
* Define more complex clause that will always be applied to the report query
*
* @param string $where
* @param array $params Note that the param names should be generated by {@see database::generate_param_name}
*/
final public function add_base_condition_sql(string $where, array $params = []): void {
database::validate_params($params);
$this->sqlwheres[] = trim($where);
$this->sqlparams = $params + $this->sqlparams;
}
/**
* Return base select/params for the report query
*
* @return array [string $select, array $params]
*/
final public function get_base_condition(): array {
return [
implode(' AND ', $this->sqlwheres),
$this->sqlparams,
];
}
/**
* Adds given entity, along with it's columns and filters, to the report
*
* @param entity_base $entity
*/
final protected function add_entity(entity_base $entity): void {
$entityname = $entity->get_entity_name();
$this->annotate_entity($entityname, $entity->get_entity_title());
$this->entities[$entityname] = $entity->initialise();
}
/**
* Define a new entity for the report
*
* @param string $name
* @param lang_string $title
* @throws coding_exception
*/
final protected function annotate_entity(string $name, lang_string $title): void {
if (empty($name) || $name !== clean_param($name, PARAM_ALPHANUMEXT)) {
throw new coding_exception('Entity name must be comprised of alphanumeric character, underscore or dash');
}
$this->entitytitles[$name] = $title;
}
/**
* Adds a column to the report
*
* @param column $column
* @return column
* @throws coding_exception
*/
final protected function add_column(column $column): column {
if (!array_key_exists($column->get_entity_name(), $this->entitytitles)) {
throw new coding_exception('Invalid entity name', $column->get_entity_name());
}
$name = $column->get_name();
if (empty($name) || $name !== clean_param($name, PARAM_ALPHANUMEXT)) {
throw new coding_exception('Column name must be comprised of alphanumeric character, underscore or dash');
}
$uniqueidentifier = $column->get_unique_identifier();
if (array_key_exists($uniqueidentifier, $this->columns)) {
throw new coding_exception('Duplicate column identifier', $uniqueidentifier);
}
$this->columns[$uniqueidentifier] = $column;
return $column;
}
/**
* Add given columns to the report from one or more entities
*
* Each entity must have already been added to the report before calling this method
*
* @param string[] $columns Unique identifier of each entity column
* @throws coding_exception For unknown entities
*/
final protected function add_columns_from_entities(array $columns): void {
foreach ($columns as $column) {
[$entityname, $columnname] = explode(':', $column, 2);
if (!array_key_exists($entityname, $this->entities)) {
throw new coding_exception('Invalid entity name', $entityname);
}
$this->add_column($this->entities[$entityname]->get_column($columnname));
}
}
/**
* Return report column by unique identifier
*
* @param string $uniqueidentifier
* @return column|null
*/
final public function get_column(string $uniqueidentifier): ?column {
return $this->columns[$uniqueidentifier] ?? null;
}
/**
* Return all available report columns
*
* @return column[]
*/
final public function get_columns(): array {
return array_filter($this->columns, static function(column $column): bool {
return $column->get_is_available();
});
}
/**
* Adds a filter to the report
*
* @param filter $filter
* @return filter
* @throws coding_exception
*/
final protected function add_filter(filter $filter): filter {
if (!array_key_exists($filter->get_entity_name(), $this->entitytitles)) {
throw new coding_exception('Invalid entity name', $filter->get_entity_name());
}
$name = $filter->get_name();
if (empty($name) || $name !== clean_param($name, PARAM_ALPHANUMEXT)) {
throw new coding_exception('Filter name must be comprised of alphanumeric character, underscore or dash');
}
$uniqueidentifier = $filter->get_unique_identifier();
if (array_key_exists($uniqueidentifier, $this->filters)) {
throw new coding_exception('Duplicate filter identifier', $uniqueidentifier);
}
$this->filters[$uniqueidentifier] = $filter;
return $filter;
}
/**
* Add given filters to the report from one or more entities
*
* Each entity must have already been added to the report before calling this method
*
* @param string[] $filters Unique identifier of each entity filter
* @throws coding_exception For unknown entities
*/
final protected function add_filters_from_entities(array $filters): void {
foreach ($filters as $filter) {
[$entityname, $filtername] = explode(':', $filter, 2);
if (!array_key_exists($entityname, $this->entities)) {
throw new coding_exception('Invalid entity name', $entityname);
}
$this->add_filter($this->entities[$entityname]->get_filter($filtername));
}
}
/**
* Return report filter by unique identifier
*
* @param string $uniqueidentifier
* @return filter|null
*/
final public function get_filter(string $uniqueidentifier): ?filter {
return $this->filters[$uniqueidentifier] ?? null;
}
/**
* Return all available report filters
*
* @return filter[]
*/
final public function get_filters(): array {
return array_filter($this->filters, static function(filter $filter): bool {
return $filter->get_is_available();
});
}
/**
* Return all report filter instances
*
* @return filter_base[]
*/
final public function get_filter_instances(): array {
return array_map(static function(filter $filter): filter_base {
/** @var filter_base $filterclass */
$filterclass = $filter->get_filter_class();
return $filterclass::create($filter);
}, $this->get_filters());
}
/**
* Set the filter values of the report
*
* @param array $values
* @return bool
*/
final public function set_filter_values(array $values): bool {
return user_filter_manager::set($this->report->get('id'), $values);
}
/**
* Get the filter values of the report
*
* @return array
*/
final public function get_filter_values(): array {
return user_filter_manager::get($this->report->get('id'));
}
/**
* Set if the report can be downloaded.
*
* @param bool $downloadable
* @param string $downloadfilename If the report is downloadable, then a filename should be provided here
*/
final public function set_downloadable(bool $downloadable, string $downloadfilename = 'export'): void {
$this->downloadable = $downloadable;
$this->downloadfilename = $downloadfilename;
}
/**
* Get if the report can be downloaded.
*
* @return bool
*/
final public function is_downloadable(): bool {
return $this->downloadable;
}
/**
* Return the downloadable report filename
*
* @return string
*/
final public function get_downloadfilename(): string {
return $this->downloadfilename;
}
/**
* Returns the report context
*
* @return context
*/
public function get_context(): context {
return $this->report->get_context();
}
}
@@ -0,0 +1,544 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\local\report;
use coding_exception;
use lang_string;
use core_reportbuilder\local\helpers\database;
/**
* Class to represent a report column
*
* @package core_reportbuilder
* @copyright 2020 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
final class column {
/** @var int Column type is integer */
public const TYPE_INTEGER = 1;
/** @var int Column type is text */
public const TYPE_TEXT = 2;
/** @var int Column type is timestamp */
public const TYPE_TIMESTAMP = 3;
/** @var int Column type is boolean */
public const TYPE_BOOLEAN = 4;
/** @var int Column type is float */
public const TYPE_FLOAT = 5;
/** @var int Column type is long text */
public const TYPE_LONGTEXT = 6;
/** @var int $index Column index within a report */
private $index;
/** @var string $columnname Internal reference to name of column */
private $columnname;
/** @var lang_string $columntitle Used as a title for the column in reports */
private $columntitle;
/** @var string $entityname Name of the entity this column belongs to */
private $entityname;
/** @var int $type Column data type (one of the TYPE_* class constants) */
private $type = null;
/** @var string[] $joins List of SQL joins for this column */
private $joins = [];
/** @var array $fields */
private $fields = [];
/** @var array $params */
private $params = [];
/** @var array[] $callbacks Array of [callable, additionalarguments] */
private $callbacks = [];
/** @var bool $issortable Used to indicate if a column is sortable */
private $issortable = false;
/** @var array $attributes */
private $attributes = [];
/** @var bool $available Used to know if column is available to the current user or not */
protected $available = true;
/**
* Column constructor
*
* For better readability use chainable methods, for example:
*
* $report->add_column(
* (new column('name', new lang_string('name'), 'user'))
* ->add_join('left join {table} t on t.id = p.tableid')
* ->add_field('t.name')
* ->add_callback([format::class, 'format_string']));
*
* @param string $name Internal name of the column
* @param lang_string|null $title Title of the column used in reports (null for blank)
* @param string $entityname Name of the entity this column belongs to. Typically when creating columns within entities
* this value should be the result of calling {@see get_entity_name}, however if creating columns inside reports directly
* it should be the name of the entity as passed to {@see \core_reportbuilder\local\report\base::annotate_entity}
*/
public function __construct(string $name, ?lang_string $title, string $entityname) {
$this->columnname = $name;
$this->columntitle = $title;
$this->entityname = $entityname;
}
/**
* Set column name
*
* @param string $name
* @return self
*/
public function set_name(string $name): self {
$this->columnname = $name;
return $this;
}
/**
* Return column name
*
* @return mixed
*/
public function get_name(): string {
return $this->columnname;
}
/**
* Set column title
*
* @param lang_string|null $title
* @return self
*/
public function set_title(?lang_string $title): self {
$this->columntitle = $title;
return $this;
}
/**
* Return column title
*
* @return string
*/
public function get_title(): string {
return $this->columntitle ? (string) $this->columntitle : '';
}
/**
* Get column entity name
*
* @return string
*/
public function get_entity_name(): string {
return $this->entityname;
}
/**
* Return unique identifier for this column
*
* @return string
*/
public function get_unique_identifier(): string {
return $this->get_entity_name() . ':' . $this->get_name();
}
/**
* Set the column index within the current report
*
* @param int $index
* @return column
*/
public function set_index(int $index): self {
$this->index = $index;
return $this;
}
/**
* Set the column type
*
* @param int $type
* @return column
* @throws coding_exception
*/
public function set_type(int $type): self {
$allowedtypes = [
self::TYPE_INTEGER,
self::TYPE_TEXT,
self::TYPE_TIMESTAMP,
self::TYPE_BOOLEAN,
self::TYPE_FLOAT,
self::TYPE_LONGTEXT,
];
if (!in_array($type, $allowedtypes)) {
throw new coding_exception('Invalid column type', $type);
}
$this->type = $type;
return $this;
}
/**
* Return column type
*
* @return int|null
*/
public function get_type(): ?int {
return $this->type;
}
/**
* Add join clause required for this column to join to existing tables/entities
*
* This is necessary in the case where {@see add_field} is selecting data from a table that isn't otherwise queried
*
* @param string $join
* @return self
*/
public function add_join(string $join): self {
$this->joins[trim($join)] = trim($join);
return $this;
}
/**
* Add multiple join clauses required for this column, passing each to {@see add_join}
*
* Typically when defining columns in entities, you should pass {@see \core_reportbuilder\local\report\base::get_joins} to
* this method, so that all entity joins are included in the report when your column is added to it
*
* @param string[] $joins
* @return self
*/
public function add_joins(array $joins): self {
foreach ($joins as $join) {
$this->add_join($join);
}
return $this;
}
/**
* Return column joins
*
* @return string[]
*/
public function get_joins(): array {
return array_values($this->joins);
}
/**
* Adds a field to be queried from the database that is necessary for this column
*
* Multiple fields can be added per column, this method may be called several times. Field aliases must be unique inside
* any given column, but there will be no conflicts if the same aliases are used in other columns in the same report
*
* @param string $sql SQL query, this may be a simple "tablealias.fieldname" or a complex sub-query that returns only one field
* @param string $alias
* @param array $params
* @return self
* @throws coding_exception
*/
public function add_field(string $sql, string $alias = '', array $params = []): self {
database::validate_params($params);
// SQL ends with a space and a word - this looks like an alias was passed as part of the field.
if (preg_match('/ \w+$/', $sql) && empty($alias)) {
throw new coding_exception('Column alias must be passed as a separate argument', $sql);
}
// If no alias was specified, auto-detect it based on common patterns ("table.column" or just "column").
if (empty($alias) && preg_match('/^(\w+\.)?(?<fieldname>\w+)$/', $sql, $matches)) {
$alias = $matches['fieldname'];
}
if (empty($alias)) {
throw new coding_exception('Complex columns must have an alias', $sql);
}
$this->fields[$alias] = $sql;
$this->params += $params;
return $this;
}
/**
* Add a list of comma-separated fields
*
* @param string $sql
* @param array $params
* @return self
*/
public function add_fields(string $sql, array $params = []): self {
database::validate_params($params);
// Split SQL into separate fields (separated by comma).
$fields = preg_split('/\s*,\s*/', $sql);
foreach ($fields as $field) {
// Split each field into expression, <field> <as> <alias> where "as" and "alias" are optional.
$fieldparts = preg_split('/\s+/', $field);
if (count($fieldparts) == 2 || (count($fieldparts) == 3 && strtolower($fieldparts[1]) === 'as')) {
$sql = reset($fieldparts);
$alias = array_pop($fieldparts);
$this->add_field($sql, $alias);
} else {
$this->add_field($field);
}
}
$this->params += $params;
return $this;
}
/**
* Given a param name, add a unique prefix to ensure that the same column with params can be added multiple times to a report
*
* @param string $name
* @return string
*/
private function unique_param_name(string $name): string {
return "p{$this->index}_{$name}";
}
/**
* Helper method to take all fields added to the column, and return appropriate SQL and alias
*
* @return array[]
*/
private function get_fields_sql_alias(): array {
$fields = [];
foreach ($this->fields as $alias => $sql) {
// Ensure params within SQL are prefixed with column index.
foreach ($this->params as $name => $value) {
$sql = preg_replace_callback('/:(?<param>' . preg_quote($name, '\b/') . ')/', function(array $matches): string {
return ':' . $this->unique_param_name($matches['param']);
}, $sql);
}
$fields[$alias] = [
'sql' => $sql,
'alias' => substr("c{$this->index}_{$alias}", 0, 30),
];
}
return $fields;
}
/**
* Return array of SQL expressions for each field of this column
*
* @return array
*/
public function get_fields(): array {
$fields = array_map(static function(array $field): string {
return "{$field['sql']} AS {$field['alias']}";
}, $this->get_fields_sql_alias());
return array_values($fields);
}
/**
* Return column parameters, prefixed by the current index to allow the column to be added multiple times to a report
*
* @return array
*/
public function get_params(): array {
$params = [];
foreach ($this->params as $name => $value) {
$paramname = $this->unique_param_name($name);
$params[$paramname] = $value;
}
return $params;
}
/**
* Return an alias for this column (the generated alias of it's first field)
*
* @return string
* @throws coding_exception
*/
public function get_column_alias(): string {
if (!$fields = $this->get_fields_sql_alias()) {
throw new coding_exception('Column ' . $this->get_unique_identifier() . ' contains no fields');
}
return reset($fields)['alias'];
}
/**
* Adds column callback (in the case there are multiple, they will be applied one after another)
*
* The callback should implement the following signature (where $value is the first column field, $row is all column
* fields, and $additionalarguments are those passed on from this method):
*
* function($value, stdClass $row[, $additionalarguments]): string
*
* @param callable $callable function that takes arguments ($value, \stdClass $row, $additionalarguments)
* @param mixed $additionalarguments
* @return self
*/
public function add_callback(callable $callable, $additionalarguments = null): self {
$this->callbacks[] = [$callable, $additionalarguments];
return $this;
}
/**
* Sets column callback. This will overwrite any previously added callbacks {@see add_callback}
*
* @param callable $callable
* @param mixed $additionalarguments
* @return self
*/
public function set_callback(callable $callable, $additionalarguments = null): self {
$this->callbacks = [];
return $this->add_callback($callable, $additionalarguments);
}
/**
* Sets the column as sortable
*
* @param bool $issortable
* @return self
*/
public function set_is_sortable(bool $issortable): self {
$this->issortable = $issortable;
return $this;
}
/**
* Return sortable status of column
*
* @return bool
*/
public function get_is_sortable(): bool {
return $this->issortable;
}
/**
* Extract all values from given row for this column
*
* @param array $row
* @return array
*/
private function get_values(array $row): array {
$values = [];
foreach ($this->get_fields_sql_alias() as $alias => $field) {
$values[$alias] = $row[$field['alias']];
}
return $values;
}
/**
* Return the default column value, that being the value of it's first field
*
* @param array $values
* @return mixed
*/
private function get_default_value(array $values) {
$value = reset($values);
// Ensure default value is cast to it's strict type.
switch ($this->get_type()) {
case self::TYPE_INTEGER:
case self::TYPE_TIMESTAMP:
$value = (int) $value;
break;
case self::TYPE_FLOAT:
$value = (float) $value;
break;
case self::TYPE_BOOLEAN:
$value = (bool) $value;
break;
}
return $value;
}
/**
* Return column value based on complete table row
*
* @param array $row
* @return mixed
*/
public function format_value(array $row) {
$values = $this->get_values($row);
$value = $this->get_default_value($values);
// Loop through, and apply any defined callbacks.
foreach ($this->callbacks as $callback) {
$value = ($callback[0])($value, (object) $values, $callback[1]);
}
return $value;
}
/**
* Add column attributes (data-, class, etc.) that will be included in HTML when column is displayed
*
* @param array $attributes
* @return self
*/
public function add_attributes(array $attributes): self {
$this->attributes = $attributes + $this->attributes;
return $this;
}
/**
* Returns the column HTML attributes
*
* @return array
*/
public function get_attributes(): array {
return $this->attributes;
}
/**
* Return available state of the column for the current user. For instance the column may be added to a report with the
* expectation that only some users are able to see it
*
* @return bool
*/
public function get_is_available(): bool {
return $this->available;
}
/**
* Conditionally set whether the column is available.
*
* @param bool $available
* @return self
*/
public function set_is_available(bool $available): self {
$this->available = $available;
return $this;
}
}
@@ -0,0 +1,310 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\local\report;
use lang_string;
use moodle_exception;
use core_reportbuilder\local\filters\base;
/**
* Class to represent a report filter
*
* @package core_reportbuilder
* @copyright 2021 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
final class filter {
/** @var string $filterclass */
private $filterclass;
/** @var string $name */
private $name;
/** @var lang_string $header */
private $header;
/** @var string $entity */
private $entityname;
/** @var string $fieldsql */
private $fieldsql = '';
/** @var array $fieldparams */
private $fieldparams = [];
/** @var string[] $joins */
protected $joins = [];
/** @var bool $available */
protected $available = true;
/** @var mixed $options */
protected $options;
/** @var array $limitoperators */
protected $limitoperators = [];
/**
* Filter constructor
*
* @param string $filterclass Filter type class to use, must extend {@see base} filter class
* @param string $name Internal name of the filter
* @param lang_string $header Title of the filter used in reports
* @param string $entityname Name of the entity this filter belongs to. Typically when creating filters within entities
* this value should be the result of calling {@see get_entity_name}, however if creating filters inside reports directly
* it should be the name of the entity as passed to {@see \core_reportbuilder\local\report\base::annotate_entity}
* @param string $fieldsql SQL clause to use for filtering, {@see set_field_sql}
* @param array $fieldparams
* @throws moodle_exception For invalid filter class
*/
public function __construct(
string $filterclass,
string $name,
lang_string $header,
string $entityname,
string $fieldsql = '',
array $fieldparams = []
) {
if (!class_exists($filterclass) || !is_subclass_of($filterclass, base::class)) {
throw new moodle_exception('filterinvalid', 'reportbuilder', '', null, $filterclass);
}
$this->filterclass = $filterclass;
$this->name = $name;
$this->header = $header;
$this->entityname = $entityname;
if ($fieldsql !== '') {
$this->set_field_sql($fieldsql, $fieldparams);
}
}
/**
* Get filter class path
*
* @return string
*/
public function get_filter_class(): string {
return $this->filterclass;
}
/**
* Get filter name
*
* @return string
*/
public function get_name(): string {
return $this->name;
}
/**
* Return header
*
* @return string
*/
public function get_header(): string {
return $this->header->out();
}
/**
* Set header
*
* @param lang_string $header
* @return self
*/
public function set_header(lang_string $header): self {
$this->header = $header;
return $this;
}
/**
* Return filter entity name
*
* @return string
*/
public function get_entity_name(): string {
return $this->entityname;
}
/**
* Return unique identifier for this filter
*
* @return string
*/
public function get_unique_identifier(): string {
return $this->get_entity_name() . ':' . $this->get_name();
}
/**
* Return joins
*
* @return string[]
*/
public function get_joins(): array {
return array_values($this->joins);
}
/**
* Add join clause required for this filter to join to existing tables/entities
*
* This is necessary in the case where {@see set_field_sql} is selecting data from a table that isn't otherwise queried
*
* @param string $join
* @return self
*/
public function add_join(string $join): self {
$this->joins[trim($join)] = trim($join);
return $this;
}
/**
* Add multiple join clauses required for this filter, passing each to {@see add_join}
*
* Typically when defining filters in entities, you should pass {@see \core_reportbuilder\local\report\base::get_joins} to
* this method, so that all entity joins are included in the report when your filter is used in it
*
* @param string[] $joins
* @return self
*/
public function add_joins(array $joins): self {
foreach ($joins as $join) {
$this->add_join($join);
}
return $this;
}
/**
* Get SQL expression for the field
*
* @return string
*/
public function get_field_sql(): string {
return $this->fieldsql;
}
/**
* Get the SQL params for the field being filtered
*
* @return array
*/
public function get_field_params(): array {
return $this->fieldparams;
}
/**
* Set the SQL expression for the field that is being filtered. It will be passed to the filter class
*
* @param string $sql
* @param array $params
* @return self
*/
public function set_field_sql(string $sql, array $params = []): self {
$this->fieldsql = $sql;
$this->fieldparams = $params;
return $this;
}
/**
* Return available state of the filter for the current user
*
* @return bool
*/
public function get_is_available(): bool {
return $this->available;
}
/**
* Conditionally set whether the filter is available. For instance the filter may be added to a report with the
* expectation that only some users are able to see it
*
* @param bool $available
* @return self
*/
public function set_is_available(bool $available): self {
$this->available = $available;
return $this;
}
/**
* Set the options for the filter in the format that the filter class expected (e.g. the "select" filter expects an array)
*
* This method should only be used if the options do not require any calculations/queries, in which
* case {@see set_options_callback} should be used. For performance, {@see get_string} shouldn't be used either, use of
* {@see lang_string} is instead encouraged
*
* @param mixed $options
* @return self
*/
public function set_options($options): self {
$this->options = $options;
return $this;
}
/**
* Set the options for the filter to be returned by a callback (that recieves no arguments) in the format that the filter
* class expects
*
* @param callable $callback
* @return self
*/
public function set_options_callback(callable $callback): self {
$this->options = $callback;
return $this;
}
/**
* Get the options for the filter, returning via the the previously set options or generated via defined options callback
*
* @return mixed
*/
public function get_options() {
if (is_callable($this->options)) {
$callable = $this->options;
$this->options = ($callable)();
}
return $this->options;
}
/**
* Set a limited subset of operators that should be used for the filter, refer to each filter class to find defined
* operator constants
*
* @param array $limitoperators Simple array of operator values
* @return self
*/
public function set_limited_operators(array $limitoperators): self {
$this->limitoperators = $limitoperators;
return $this;
}
/**
* Filter given operators to include only those previously defined by {@see set_limited_operators}
*
* @param array $operators All operators as defined by the filter class
* @return array
*/
public function restrict_limited_operators(array $operators): array {
if (empty($this->limitoperators)) {
return $operators;
}
return array_intersect_key($operators, array_flip($this->limitoperators));
}
}
+87
View File
@@ -0,0 +1,87 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder;
use stdClass;
use core_reportbuilder\local\models\report;
use core_reportbuilder\local\report\base;
/**
* Report management class
*
* @package core_reportbuilder
* @copyright 2020 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class manager {
/**
* Return an instance of a report class from the given report persistent
*
* @param report $report
* @param array $parameters
* @return base
* @throws source_invalid_exception
* @throws source_unavailable_exception
*/
public static function get_report_from_persistent(report $report, array $parameters = []): base {
$source = $report->get('source');
// Throw exception for invalid or unavailable report source.
if (!self::report_source_exists($source)) {
throw new source_invalid_exception($source);
} else if (!self::report_source_available($source)) {
throw new source_unavailable_exception($source);
}
return new $source($report, $parameters);
}
/**
* Verify that report source exists and extends appropriate base classes
*
* @param string $source Full namespaced path to report definition
* @param string $additionalbaseclass Specify addition base class that given classname should extend
* @return bool
*/
public static function report_source_exists(string $source, string $additionalbaseclass = ''): bool {
return (class_exists($source) && is_subclass_of($source, base::class) &&
(empty($additionalbaseclass) || is_subclass_of($source, $additionalbaseclass)));
}
/**
* Verify given report source is available. Note that it is assumed caller has already checked that it exists
*
* @param string $source
* @return bool
*/
public static function report_source_available(string $source): bool {
return call_user_func([$source, 'is_available']);
}
/**
* Create new report persistent
*
* @param stdClass $reportdata
* @return report
*/
public static function create_report_persistent(stdClass $reportdata): report {
return (new report(0, $reportdata))->create();
}
}
+59
View File
@@ -0,0 +1,59 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\output;
use plugin_renderer_base;
use core_reportbuilder\table\system_report_table;
/**
* Report renderer class
*
* @package core_reportbuilder
* @copyright 2020 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class renderer extends plugin_renderer_base {
/**
* Render a system report
*
* @param system_report $report
* @return string
*/
protected function render_system_report(system_report $report): string {
$context = $report->export_for_template($this);
return $this->render_from_template('core_reportbuilder/system_report', $context);
}
/**
* Render a system report table
*
* @param system_report_table $table
* @return string
*/
protected function render_system_report_table(system_report_table $table): string {
ob_start();
$table->out($table->get_default_per_page(), false);
$output = ob_get_contents();
ob_end_clean();
return $output;
}
}
@@ -0,0 +1,74 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\output;
use renderable;
use renderer_base;
use stdClass;
use templatable;
use core_reportbuilder\external\system_report_exporter;
use core_reportbuilder\local\models\report;
use core_reportbuilder\local\report\base;
/**
* System report output class
*
* @package core_reportbuilder
* @copyright 2020 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class system_report implements renderable, templatable {
/** @var report $report */
protected $report;
/** @var base $source */
protected $source;
/** @var array $parameters */
protected $parameters;
/**
* Class constructor
*
* @param report $report
* @param base $source
* @param array $parameters
*/
public function __construct(report $report, base $source, array $parameters) {
$this->report = $report;
$this->source = $source;
$this->parameters = $parameters;
}
/**
* Export report data suitable for a template
*
* @param renderer_base $output
* @return stdClass
*/
public function export_for_template(renderer_base $output): stdClass {
$exporter = new system_report_exporter($this->report, [
'source' => $this->source,
'parameters' => json_encode($this->parameters),
]);
return $exporter->export($output);
}
}
@@ -0,0 +1,72 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\privacy;
use core_privacy\local\metadata\collection;
use core_privacy\local\request\writer;
use core_reportbuilder\local\helpers\user_filter_manager;
use core_reportbuilder\local\models\report;
/**
* Privacy Subsystem for core_reportbuilder
*
* @package core_reportbuilder
* @copyright 2021 David Matamoros <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements
\core_privacy\local\metadata\provider,
\core_privacy\local\request\user_preference_provider {
/**
* Returns metadata about the component
*
* @param collection $collection
* @return collection
*/
public static function get_metadata(collection $collection): collection {
$collection->add_database_table(report::TABLE, [
'name' => 'privacy:metadata:report:name',
'usercreated' => 'privacy:metadata:report:usercreated',
'usermodified' => 'privacy:metadata:report:usermodified',
], 'privacy:metadata:report');
$collection->add_user_preference('core_reportbuilder', 'privacy:metadata:preference:reportfilter');
return $collection;
}
/**
* Export all user preferences for the component
*
* @param int $userid
*/
public static function export_user_preferences(int $userid): void {
$preferencestring = get_string('privacy:metadata:preference:reportfilter', 'core_reportbuilder');
$filters = user_filter_manager::get_all_for_user($userid);
foreach ($filters as $key => $filter) {
writer::export_user_preference('core_reportbuilder',
$key,
json_encode($filter, JSON_PRETTY_PRINT),
$preferencestring
);
}
}
}
@@ -0,0 +1,38 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder;
use moodle_exception;
/**
* User cannot access report exception
*
* @package core_reportbuilder
* @copyright 2021 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class report_access_exception extends moodle_exception {
/**
* Constructor
*/
public function __construct() {
parent::__construct('errorreportaccess', 'reportbuilder');
}
}
@@ -0,0 +1,40 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder;
use moodle_exception;
/**
* Invalid report source exception
*
* @package core_reportbuilder
* @copyright 2020 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class source_invalid_exception extends moodle_exception {
/**
* Constructor
*
* @param string $source
*/
public function __construct(string $source) {
parent::__construct('errorsourceinvalid', 'reportbuilder', '', null, $source);
}
}
@@ -0,0 +1,40 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder;
use moodle_exception;
/**
* Unavailable report source exception
*
* @package core_reportbuilder
* @copyright 2020 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class source_unavailable_exception extends moodle_exception {
/**
* Constructor
*
* @param string $source
*/
public function __construct(string $source) {
parent::__construct('errorsourceunavailable', 'reportbuilder', '', null, $source);
}
}
+282
View File
@@ -0,0 +1,282 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder;
use coding_exception;
use stdClass;
use core_reportbuilder\local\models\report;
use core_reportbuilder\local\report\action;
use core_reportbuilder\local\report\base;
use core_reportbuilder\local\report\column;
/**
* Base class for system reports
*
* @package core_reportbuilder
* @copyright 2020 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class system_report extends base {
/** @var array $parameters */
private $parameters;
/** @var string[] $basefields List of base fields */
private $basefields = [];
/** @var action[] $actions */
private $actions = [];
/** @var column $initialsortcolumn */
private $initialsortcolumn;
/** @var int $initialsortdirection */
private $initialsortdirection;
/**
* System report constructor.
*
* @param report $report
* @param array $parameters
*/
final public function __construct(report $report, array $parameters) {
$this->parameters = $parameters;
parent::__construct($report);
}
/**
* Validates access to view this report
*
* This is necessary to implement independently of the page that would typically embed the report because
* subsequent pages are requested via AJAX requests, and access should be validated each time
*
* @return bool
*/
abstract protected function can_view(): bool;
/**
* Validate access to the report
*
* @throws report_access_exception
*/
final public function require_can_view(): void {
if (!$this->can_view()) {
throw new report_access_exception();
}
}
/**
* Report validation
*
* @throws report_access_exception If user cannot access the report
* @throws coding_exception If no default column are specified
*/
protected function validate(): void {
parent::validate();
$this->require_can_view();
// Ensure the report has some default columns specified.
if (empty($this->get_columns())) {
throw new coding_exception('No columns added');
}
}
/**
* Add list of fields that have to be always included in SQL query for actions and row classes
*
* Base fields are only available in system reports because they are not compatible with aggregation
*
* @param string $sql SQL clause for the list of fields that only uses main table or base joins
*/
final protected function add_base_fields(string $sql): void {
$this->basefields[] = $sql;
}
/**
* Return report base fields
*
* @return array
*/
final public function get_base_fields(): array {
return $this->basefields;
}
/**
* Adds an action to the report
*
* @param action $action
*/
final public function add_action(action $action): void {
$this->actions[] = $action;
}
/**
* Whether report has any actions
*
* @return bool
*/
final public function has_actions(): bool {
return !empty($this->actions);
}
/**
* Return report actions
*
* @return action[]
*/
final public function get_actions(): array {
return $this->actions;
}
/**
* Set all report parameters
*
* @param array $parameters
*/
final public function set_parameters(array $parameters): void {
$this->parameters = $parameters;
}
/**
* Return all report parameters
*
* @return array
*/
final public function get_parameters(): array {
return $this->parameters;
}
/**
* Return specific report parameter
*
* @param string $param
* @param mixed $default
* @param string $type
* @return mixed
*/
final public function get_parameter(string $param, $default, string $type) {
if (!array_key_exists($param, $this->parameters)) {
return $default;
}
return clean_param($this->parameters[$param], $type);
}
/**
* Output the report
*
* @uses \core_reportbuilder\output\renderer::render_system_report()
*
* @return string
*/
final public function output(): string {
global $PAGE;
/** @var \core_reportbuilder\output\renderer $renderer */
$renderer = $PAGE->get_renderer('core_reportbuilder');
$report = new \core_reportbuilder\output\system_report($this->get_report_persistent(), $this, $this->parameters);
return $renderer->render($report);
}
/**
* CSS classes to add to the row. Can be overridden by system reports do define class to be added to output according to
* content of each row
*
* @param stdClass $row
* @return string
*/
public function get_row_class(stdClass $row): string {
return '';
}
/**
* Default 'per page' size. Can be overridden by system reports to define a different paging value
*
* @return int
*/
public function get_default_per_page(): int {
return self::DEFAULT_PAGESIZE;
}
/**
* Called before rendering each row. Can be overridden to pre-fetch/create objects and store them in the class, which can
* later be used in column and action callbacks
*
* @param stdClass $row
*/
public function row_callback(stdClass $row): void {
return;
}
/**
* Validates access to download this report.
*
* @return bool
*/
final public function can_be_downloaded(): bool {
return $this->can_view() && $this->is_downloadable();
}
/**
* Return list of column names that will be excluded when table is downloaded. Extending classes should override this method
* as appropriate
*
* @return string[] Array of column unique identifiers
*/
public function get_exclude_columns_for_download(): array {
return [];
}
/**
* Set initial sort column and sort direction for the report
*
* @param string $uniqueidentifier
* @param int $sortdirection One of SORT_ASC or SORT_DESC
* @throws coding_exception
*/
public function set_initial_sort_column(string $uniqueidentifier, int $sortdirection): void {
if (!$sortcolumn = $this->get_column($uniqueidentifier)) {
throw new coding_exception('Unknown column identifier', $uniqueidentifier);
}
$this->initialsortcolumn = $sortcolumn;
$this->initialsortdirection = $sortdirection;
}
/**
* Get initial sort column
*
* @return column|null
*/
public function get_initial_sort_column(): ?column {
return $this->initialsortcolumn;
}
/**
* Get initial sort column direction
*
* @return int
*/
public function get_initial_sort_direction(): int {
return $this->initialsortdirection;
}
}
@@ -0,0 +1,87 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder;
use context;
use core_reportbuilder\local\models\report;
use core_reportbuilder\local\report\base;
/**
* Factory class for creating system report instances
*
* @package core_reportbuilder
* @copyright 2020 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class system_report_factory {
/**
* Create and return instance of given system report source
*
* @param string $source Class path of system report definition
* @param context $context
* @param string $component
* @param string $area
* @param int $itemid
* @param array $parameters Simple key/value pairs, accessed inside reports using $this->get_parameter()
* @return system_report
* @throws source_invalid_exception
*/
public static function create(string $source, context $context, string $component = '', string $area = '',
int $itemid = 0, array $parameters = []): system_report {
// Exit early if source isn't a system report.
if (!manager::report_source_exists($source, system_report::class)) {
throw new source_invalid_exception($source);
}
$report = static::get_report_persistent($source, $context, $component, $area, $itemid);
return manager::get_report_from_persistent($report, $parameters);
}
/**
* Given a report source, with accompanying context information, return a persistent report instance
*
* @param string $source
* @param context $context
* @param string $component
* @param string $area
* @param int $itemid
* @return report
*/
private static function get_report_persistent(string $source, context $context, string $component = '', string $area = '',
int $itemid = 0): report {
$reportdata = [
'type' => base::TYPE_SYSTEM_REPORT,
'source' => $source,
'contextid' => $context->id,
'component' => $component,
'area' => $area,
'itemid' => $itemid,
];
if ($report = report::get_record($reportdata)) {
return $report;
}
return manager::create_report_persistent((object) $reportdata);
}
}
@@ -0,0 +1,329 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\table;
use context;
use html_writer;
use moodle_exception;
use moodle_url;
use renderable;
use table_sql;
use stdClass;
use core_table\dynamic;
use core_reportbuilder\manager;
use core_reportbuilder\system_report;
use core_reportbuilder\local\filters\base;
use core_reportbuilder\local\models\report;
use core_reportbuilder\local\report\action;
use core_reportbuilder\local\report\column;
use core_table\local\filter\filterset;
defined('MOODLE_INTERNAL') || die;
require_once("{$CFG->libdir}/tablelib.php");
/**
* System report dynamic table class
*
* @package core_reportbuilder
* @copyright 2020 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class system_report_table extends table_sql implements dynamic, renderable {
/** @var string Unique ID prefix for the table */
private const UNIQUEID_PREFIX = 'system-report-table-';
/** @var report $report */
protected $report;
/** @var system_report $systemreport */
protected $systemreport;
/**
* Table constructor. Note that the passed unique ID value must match the pattern "system-report-table-(\d+)" so that
* dynamic updates continue to load the same report
*
* @param string $uniqueid
* @param array $parameters
* @throws moodle_exception For invalid unique ID
*/
public function __construct(string $uniqueid, array $parameters = []) {
if (!preg_match('/^' . self::UNIQUEID_PREFIX . '(?<id>\d+)$/', $uniqueid, $matches)) {
throw new moodle_exception('invalidsystemreportid', 'core_reportbuilder', '', null, $uniqueid);
}
parent::__construct($uniqueid);
// Load the report persistent, and accompanying system report instance.
$this->report = new report($matches['id']);
$this->systemreport = manager::get_report_from_persistent($this->report, $parameters);
$fields = $this->systemreport->get_base_fields();
$maintable = $this->systemreport->get_main_table();
$maintablealias = $this->systemreport->get_main_table_alias();
$joins = $this->systemreport->get_joins();
[$where, $params] = $this->systemreport->get_base_condition();
$this->set_attribute('data-region', 'reportbuilder-table');
$this->set_attribute('class', $this->attributes['class'] . ' reportbuilder-table');
// Download options.
$this->showdownloadbuttonsat = [TABLE_P_BOTTOM];
$this->is_downloading($parameters['download'] ?? null, $this->systemreport->get_downloadfilename());
// Retrieve all report columns. If we are downloading the report, remove as required.
$columns = $this->systemreport->get_columns();
if ($this->is_downloading()) {
$columns = array_diff_key($columns,
array_flip($this->systemreport->get_exclude_columns_for_download()));
}
$columnheaders = [];
$columnindex = 1;
foreach ($columns as $identifier => $column) {
$column->set_index($columnindex++);
$columnheaders[$column->get_column_alias()] = $column->get_title();
// Add each columns fields, joins and params to our report.
$fields = array_merge($fields, $column->get_fields());
$joins = array_merge($joins, $column->get_joins());
$params = array_merge($params, $column->get_params());
// Disable sorting for some columns.
if (!$column->get_is_sortable()) {
$this->no_sorting($column->get_column_alias());
}
}
// If the report has any actions then append appropriate column, note that actions are excluded during download.
if ($this->systemreport->has_actions() && !$this->is_downloading()) {
$columnheaders['actions'] = html_writer::tag('span', get_string('actions', 'core_reportbuilder'), [
'class' => 'sr-only',
]);
$this->no_sorting('actions');
}
$this->define_columns(array_keys($columnheaders));
$this->define_headers(array_values($columnheaders));
// Initial table sort column.
if ($sortcolumn = $this->systemreport->get_initial_sort_column()) {
$this->sortable(true, $sortcolumn->get_column_alias(), $this->systemreport->get_initial_sort_direction());
}
// Table configuration.
$this->initialbars(false);
$this->collapsible(false);
$this->pageable(true);
$this->set_default_per_page($this->systemreport->get_default_per_page());
// Initialise table SQL properties.
$fieldsql = implode(', ', $fields);
$this->init_sql($fieldsql, "{{$maintable}} {$maintablealias}", $joins, $where, $params);
}
/**
* Return a new instance of the class for given report ID. We include report parameters here so they are present during
* initialisation
*
* @param int $reportid
* @param array $parameters
* @return static
*/
public static function create(int $reportid, array $parameters): self {
return new static(self::UNIQUEID_PREFIX . $reportid, $parameters);
}
/**
* Set the filterset in the table class. We set the report parameters here so that they are persisted while paging
*
* @param filterset $filterset
*/
public function set_filterset(filterset $filterset): void {
$parameters = $filterset->get_filter('parameters')->current();
$this->systemreport->set_parameters((array) json_decode($parameters, true));
parent::set_filterset($filterset);
}
/**
* Initialises table SQL properties
*
* @param string $fields
* @param string $from
* @param array $joins
* @param string $where
* @param array $params
*/
protected function init_sql(string $fields, string $from, array $joins, string $where, array $params): void {
$wheres = [];
if ($where !== '') {
$wheres[] = $where;
}
$filtervalues = $this->systemreport->get_filter_values();
foreach ($this->systemreport->get_filters() as $filter) {
/** @var base $filterclass */
$filterclass = $filter->get_filter_class();
$filterinstance = $filterclass::create($filter);
[$filtersql, $filterparams] = $filterinstance->get_sql_filter($filtervalues);
if ($filtersql !== '') {
$wheres[] = "({$filtersql})";
$params = array_merge($params, $filterparams);
$joins = array_merge($joins, $filter->get_joins());
}
}
$wheresql = '1=1';
if (!empty($wheres)) {
$wheresql = implode(' AND ', $wheres);
}
// Add unique table joins.
$from .= ' ' . implode(' ', array_unique($joins));
$this->set_sql($fields, $from, $wheresql, $params);
$this->set_count_sql("SELECT COUNT(1) FROM {$from} WHERE {$wheresql}", $params);
}
/**
* Override parent method of the same, to make use of a recordset and avoid issues with duplicate values in the first column
*
* @param int $pagesize
* @param bool $useinitialsbar
*/
public function query_db($pagesize, $useinitialsbar = true) {
global $DB;
$sql = "SELECT {$this->sql->fields} FROM {$this->sql->from} WHERE {$this->sql->where}";
$sort = $this->get_sql_sort();
if ($sort) {
$sql .= " ORDER BY {$sort}";
}
if (!$this->is_downloading()) {
$this->pagesize($pagesize, $DB->count_records_sql($this->countsql, $this->countparams));
$this->rawdata = $DB->get_recordset_sql($sql, $this->sql->params, $this->get_page_start(), $this->get_page_size());
} else {
$this->rawdata = $DB->get_recordset_sql($sql, $this->sql->params);
}
}
/**
* Override parent method for retrieving row class with that defined by the system report
*
* @param array|stdClass $row
* @return string
*/
public function get_row_class($row) {
return $this->systemreport->get_row_class((object) $row);
}
/**
* Format each row of returned data, executing defined callbacks for the row and each column
*
* @param array|stdClass $row
* @return array
*/
public function format_row($row) {
$this->systemreport->row_callback((object) $row);
/** @var column[] $columnsbyalias */
$columnsbyalias = [];
// Create a lookup for convenience, indexed by column alias.
$columns = $this->systemreport->get_columns();
foreach ($columns as $column) {
$columnsbyalias[$column->get_column_alias()] = $column;
}
// Walk over the row, and for any key that matches one of our column aliases, call that columns format method.
$row = (array) $row;
array_walk($row, static function(&$value, $key) use ($row, $columnsbyalias): void {
if (array_key_exists($key, $columnsbyalias)) {
$value = $columnsbyalias[$key]->format_value($row);
}
});
if ($this->systemreport->has_actions()) {
$row['actions'] = $this->format_row_actions((object) $row);
}
return $row;
}
/**
* Return formatted actions column for the row
*
* @param stdClass $row
* @return string
*/
private function format_row_actions(stdClass $row): string {
$actions = array_map(static function(action $action) use ($row): string {
return (string) $action->get_action_link($row);
}, $this->systemreport->get_actions());
return implode('', $actions);
}
/**
* Get the context for the table (that of the report persistent)
*
* @return context
*/
public function get_context(): context {
return $this->report->get_context();
}
/**
* Set the base URL of the table to the current page URL
*/
public function guess_base_url(): void {
$this->baseurl = new moodle_url('/');
}
/**
* Get the html for the download buttons
*
* @return string
*/
public function download_buttons(): string {
global $OUTPUT;
if ($this->systemreport->can_be_downloaded() && !$this->is_downloading()) {
return $OUTPUT->download_dataformat_selector(
get_string('downloadas', 'table'),
new \moodle_url('/reportbuilder/download.php'),
'download',
[
'id' => $this->report->get('id'),
'parameters' => json_encode($this->systemreport->get_parameters()),
]
);
}
return '';
}
}
@@ -0,0 +1,42 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\table;
use core_table\local\filter\filterset;
/**
* System report dynamic table filterset class
*
* @package core_reportbuilder
* @copyright 2020 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class system_report_table_filterset extends filterset {
/**
* Get the required filters
*
* @return array.
*/
public function get_required_filters(): array {
return [
'parameters' => system_report_table_parameters_filter::class,
];
}
}
@@ -0,0 +1,32 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\table;
use core_table\local\filter\string_filter;
/**
* Class for storing given system report parameters
*
* @package core_reportbuilder
* @copyright 2021 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class system_report_table_parameters_filter extends string_filter {
}
+55
View File
@@ -0,0 +1,55 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Download a report
*
* @package core_reportbuilder
* @copyright 2021 David Matamoros <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
declare(strict_types=1);
use core_reportbuilder\system_report_factory;
require_once(__DIR__ . '/../config.php');
require_login();
$reportid = required_param('id', PARAM_INT);
$download = required_param('download', PARAM_ALPHA);
$parameters = optional_param('parameters', null, PARAM_RAW);
$reportpersistent = new \core_reportbuilder\local\models\report($reportid);
$context = $reportpersistent->get_context();
$PAGE->set_context($context);
$PAGE->set_url(new moodle_url('/reportbuilder/download.php'));
$systemreport = system_report_factory::create($reportpersistent->get('source'), $context);
if (!$systemreport->can_be_downloaded()) {
throw new \core_reportbuilder\report_access_exception();
}
// Combine original report parameters with 'download' parameter.
$reportparameters = ['download' => $download];
if ($parameters) {
$reportparameters = array_merge($reportparameters, (array) json_decode($parameters));
}
$outputreport = new \core_reportbuilder\output\system_report($reportpersistent, $systemreport, $reportparameters);
echo $PAGE->get_renderer('core_reportbuilder')->render($outputreport);
@@ -0,0 +1,53 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core_reportbuilder/filters/area
Template for system report filters area
Example context (json):
{
"id": 3,
"filtersform": "form"
}
}}
<div class="dropdown d-flex justify-content-end mb-2">
<!-- Filters button -->
<button class="btn btn-sm btn-secondary" type="button" id="dropdownFiltersButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="{{#str}} filters, moodle {{/str}}">
{{#pix}} i/filter, core {{/pix}}
{{#str}} filters, moodle {{/str}}
</button>
<!-- Filters content -->
<div class="filters-dropdown dropdown-menu dropdown-menu-right" aria-labelledby="dropdownFiltersButton">
<div id="filters" class="reportbuilder-filters-sidebar" data-region="report-filters">
<div class="p-3">
{{#filtersform}}
<div class="reportbuilder-filters-wrapper" data-region="filters-form">
{{{filtersform}}}
</div>
{{/filtersform}}
</div>
</div>
</div>
</div>
{{#js}}
require(['core_reportbuilder/filters'], function(filters) {
filters.init('{{id}}');
});
{{/js}}
@@ -0,0 +1,26 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core_reportbuilder/local/filters/footer
Template for the footer of a filter instance within the filter form
Example context (json):
{
}
}}
</div>
@@ -0,0 +1,32 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core_reportbuilder/local/filters/header
Template for the header of a filter instance within the filter form
Example context (json):
{
"name": "Date modified"
}
}}
<div class="filter pb-4 mb-3 border-bottom" data-filter-for="{{name}}">
<div class="filter-header d-flex align-items-start justify-content-between">
<div class="filter-name">
{{name}}
</div>
</div>
@@ -0,0 +1,49 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core_reportbuilder/system_report
Template for a system report
Example context (json):
{
"id": 1,
"source": "some\\class\\name",
"parameters": [],
"table": "table",
"filterspresent": true,
"filtersform": "form"
}
}}
<div data-region="core_reportbuilder/system-report"
data-reportid="{{id}}"
data-source="{{source}}"
data-parameter="{{parameters}}">
<div class="reportbuilder-table-wrapper">
{{#filterspresent}}
{{>core_reportbuilder/local/filters/area}}
{{/filterspresent}}
<!-- Table -->
{{{table}}}
</div>
</div>
{{#js}}
require(['core_reportbuilder/report'], function(report) {
report.init('{{id}}');
});
{{/js}}
@@ -0,0 +1,43 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
require_once(__DIR__ . '/../../../lib/behat/behat_base.php');
/**
* Behat step definitions for Reportbuilder
*
* @package core_reportbuilder
* @copyright 2021 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_reportbuilder extends behat_base {
/**
* Return the list of partial named selectors
*
* @return behat_component_named_selector[]
*/
public static function get_partial_named_selectors(): array {
return [
new behat_component_named_selector('Filter', [
".//*[@data-region='filters-form']//*[@data-filter-for=%locator%]",
]),
];
}
}
+43
View File
@@ -0,0 +1,43 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Coverage information for core_reportbuilder
*
* @package core_reportbuilder
* @copyright 2020 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
declare(strict_types=1);
defined('MOODLE_INTERNAL') || die();
return new class extends phpunit_coverage_info {
/** @var array The list of folders relative to the plugin root to include in coverage generation. */
protected $includelistfolders = [
'classes',
];
/** @var array The list of files relative to the plugin root to include in coverage generation. */
protected $includelistfiles = [];
/** @var array The list of folders relative to the plugin root to exclude in coverage generation. */
protected $excludelistfolders = [];
/** @var array The list of files relative to the plugin root to exclude in coverage generation. */
protected $excludelistfiles = [];
};
+61
View File
@@ -0,0 +1,61 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\external\filters;
use external_api;
use externallib_advanced_testcase;
use core_reportbuilder\local\helpers\user_filter_manager;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once("{$CFG->dirroot}/webservice/tests/helpers.php");
/**
* Unit tests external filters reset class
*
* @package core_reportbuilder
* @covers \core_reportbuilder\external\filters\reset
* @copyright 2021 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class reset_testcase extends externallib_advanced_testcase {
/**
* Text execute method
*/
public function test_execute(): void {
$this->resetAfterTest();
$this->setAdminUser();
user_filter_manager::set(5, [
'entity:filter_name' => 'something',
]);
$this->assertCount(1, user_filter_manager::get(5));
$result = reset::execute(5);
$result = external_api::clean_returnvalue(reset::execute_returns(), $result);
$this->assertTrue($result);
// We should get an empty array back.
$this->assertEquals([], user_filter_manager::get(5));
}
}
@@ -0,0 +1,94 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\external;
use advanced_testcase;
use context_system;
use moodle_url;
use core_reportbuilder\system_report_available;
use core_reportbuilder\system_report_factory;
/**
* Unit tests for system report exporter
*
* @package core_reportbuilder
* @covers \core_reportbuilder\external\system_report_exporter
* @copyright 2021 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class system_report_exporter_testcase extends advanced_testcase {
/**
* Load test fixture
*/
public static function setUpBeforeClass(): void {
global $CFG;
require_once("{$CFG->dirroot}/reportbuilder/tests/fixtures/system_report_available.php");
}
/**
* Data provider for {@see test_export}
*
* @return array[]
*/
public function export_provider(): array {
return [
['With filters' => true],
['Without filters' => false],
];
}
/**
* Text execute method
*
* @param bool $withfilters
*
* @dataProvider export_provider
*/
public function test_export(bool $withfilters): void {
global $PAGE;
$this->resetAfterTest();
// Prevent debug warnings from flexible_table.
$PAGE->set_url(new moodle_url('/'));
$systemreport = system_report_factory::create(system_report_available::class, context_system::instance(), '', '', 0,
['withfilters' => $withfilters]);
$exporter = new system_report_exporter($systemreport->get_report_persistent(), [
'source' => $systemreport,
'parameters' => json_encode($systemreport->get_parameters()),
]);
$data = $exporter->export($PAGE->get_renderer('core_reportbuilder'));
$this->assertNotEmpty($data->table);
if ($withfilters) {
$this->assertEquals('{"withfilters":true}', $data->parameters);
$this->assertTrue($data->filterspresent);
$this->assertNotEmpty($data->filtersform);
} else {
$this->assertEquals('{"withfilters":false}', $data->parameters);
$this->assertFalse($data->filterspresent);
$this->assertEmpty($data->filtersform);
}
}
}
+76
View File
@@ -0,0 +1,76 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder;
use core_reportbuilder\local\entities\course;
use core_reportbuilder\local\helpers\database;
/**
* Testable system report fixture for testing the course entity
*
* @package core_reportbuilder
* @copyright 2021 David Matamoros <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_entity_report extends system_report {
/**
* Initialise the report
*/
protected function initialise(): void {
$entity = new course();
$coursetablealias = $entity->get_table_alias('course');
$param = database::generate_param_name();
$this->set_main_table('course', $coursetablealias);
$this->add_entity($entity);
// Add a base condition to hide the site course.
$this->add_base_condition_sql("$coursetablealias.id <> :$param", [$param => SITEID]);
$columns = [];
foreach ($entity->get_columns() as $column) {
$columns[] = $column->get_unique_identifier();
}
$this->add_columns_from_entities($columns);
$filters = [];
foreach ($entity->get_filters() as $filter) {
$filters[] = $filter->get_unique_identifier();
}
$this->add_filters_from_entities($filters);
}
/**
* Ensure we can view the report
*
* @return bool
*/
protected function can_view(): bool {
return true;
}
/**
* Explicitly set availability of report
*
* @return bool
*/
public static function is_available(): bool {
return true;
}
}
@@ -0,0 +1,95 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder;
use core_reportbuilder\local\report\action;
use lang_string;
use core_reportbuilder\local\filters\text;
use core_reportbuilder\local\report\column;
use core_reportbuilder\local\report\filter;
use moodle_url;
use pix_icon;
/**
* Testable system report fixture
*
* @package core_reportbuilder
* @copyright 2020 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class system_report_available extends system_report {
/**
* Initialise the report
*/
protected function initialise(): void {
$this->set_main_table('user', 'u');
$this->annotate_entity('user', new lang_string('user'));
$this->add_column((new column(
'username',
new lang_string('username'),
'user'
))
->add_joins($this->get_joins())
->add_field('u.firstname')
);
$withfilters = $this->get_parameter('withfilters', false, PARAM_BOOL);
if ($withfilters) {
$this->add_filter((new filter(
text::class,
'username',
new lang_string('username'),
'user',
'u.username'
))
->add_joins($this->get_joins())
);
}
$withactions = $this->get_parameter('withactions', false, PARAM_BOOL);
if ($withactions) {
$this->add_action(new action(
new moodle_url('/user/profile.php', ['id' => ':id']),
new pix_icon('e/search', get_string('view')),
[],
true,
));
}
}
/**
* Ensure we can view the report
*
* @return bool
*/
protected function can_view(): bool {
return true;
}
/**
* Explicitly set availability of report
*
* @return bool
*/
public static function is_available(): bool {
return true;
}
}
@@ -0,0 +1,42 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/system_report_available.php');
/**
* Testable unavailable system report fixture
*
* @package core_reportbuilder
* @copyright 2020 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class system_report_unavailable extends system_report_available {
/**
* Explicitly set availability of report to false
*
* @return bool
*/
public static function is_available(): bool {
return false;
}
}
@@ -0,0 +1,75 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder;
use stdClass;
use core_reportbuilder\table\system_report_table;
/**
* Testable system report table for getting report data
*
* @package core_reportbuilder
* @copyright 2021 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class testable_system_report_table extends system_report_table {
/**
* Format each row of returned data, replacing aliased column names with the original
*
* @param array|stdClass $row
* @return array
*/
public function format_row($row): array {
$record = parent::format_row($row);
$result = [];
$columns = $this->systemreport->get_columns();
foreach ($columns as $column) {
$result[$column->get_name()] = $record[$column->get_column_alias()];
}
return $result;
}
/**
* Return all table rows
*
* @return array
*/
public function get_table_rows(): array {
global $PAGE;
$PAGE->set_url('/');
$result = [];
$this->guess_base_url();
$this->setup();
$this->query_db(0, false);
foreach ($this->rawdata as $record) {
$result[] = $this->format_row($record);
}
$this->close_recordset();
return $result;
}
}
+71
View File
@@ -0,0 +1,71 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder;
use core_reportbuilder\local\entities\user;
/**
* Testable system report fixture for testing the user entity
*
* @package core_reportbuilder
* @copyright 2021 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class user_entity_report extends system_report {
/**
* Initialise the report
*/
protected function initialise(): void {
$entity = new user();
$this->set_main_table('user', $entity->get_table_alias('user'));
$this->add_entity($entity);
$columns = [];
foreach ($entity->get_columns() as $column) {
$columns[] = $column->get_unique_identifier();
}
$this->add_columns_from_entities($columns);
$filters = [];
foreach ($entity->get_filters() as $filter) {
$filters[] = $filter->get_unique_identifier();
}
$this->add_filters_from_entities($filters);
}
/**
* Ensure we can view the report
*
* @return bool
*/
protected function can_view(): bool {
return true;
}
/**
* Explicitly set availability of report
*
* @return bool
*/
public static function is_available(): bool {
return true;
}
}
@@ -0,0 +1,475 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\local\entities;
use advanced_testcase;
use coding_exception;
use context_system;
use lang_string;
use ReflectionClass;
use core_reportbuilder\course_entity_report;
use core_reportbuilder\manager;
use core_reportbuilder\testable_system_report_table;
use core_reportbuilder\local\filters\boolean_select;
use core_reportbuilder\local\filters\date;
use core_reportbuilder\local\filters\text;
use core_reportbuilder\local\filters\select;
use core_reportbuilder\local\helpers\user_filter_manager;
/**
* Unit tests for course entity
*
* @package core_reportbuilder
* @covers \core_reportbuilder\local\entities\base
* @covers \core_reportbuilder\local\entities\course
* @covers \core_reportbuilder\local\helpers\custom_fields
* @covers \core_reportbuilder\local\report\base
* @covers \core_reportbuilder\system_report
* @copyright 2021 David Matamoros <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_testcase extends advanced_testcase {
/**
* Load required classes
*/
public static function setUpBeforeClass(): void {
global $CFG;
require_once("{$CFG->dirroot}/course/lib.php");
require_once("{$CFG->dirroot}/reportbuilder/tests/fixtures/testable_system_report_table.php");
require_once("{$CFG->dirroot}/reportbuilder/tests/fixtures/course_entity_report.php");
}
/**
* Generate courses for the tests
*/
public function generate_courses(): array {
$coursecategory1 = $this->getDataGenerator()->create_category();
$course1 = $this->getDataGenerator()->create_course([
'fullname' => 'Course 1',
'shortname' => 'C1',
'idnumber' => 'IDNumber1',
'visible' => 1,
'startdate' => 289308600,
'enddate' => 3445023600,
'category' => $coursecategory1->id,
'groupmode' => NOGROUPS,
'enablecompletion' => 1,
'downloadcontent' => DOWNLOAD_COURSE_CONTENT_DISABLED,
'format' => 'topics',
'calendartype' => 'Gregorian',
'theme' => 'afterburner',
'lang' => 'en',
]);
$coursecategory2 = $this->getDataGenerator()->create_category();
$course2 = $this->getDataGenerator()->create_course([
'fullname' => 'Course 2',
'shortname' => 'C2',
'idnumber' => 'IDNumber2',
'visible' => 0,
'startdate' => 1614726000,
'enddate' => 1961881200,
'category' => $coursecategory2->id,
'groupmode' => SEPARATEGROUPS,
'enablecompletion' => 0,
'downloadcontent' => DOWNLOAD_COURSE_CONTENT_ENABLED,
'format' => 'topics',
'calendartype' => 'Gregorian',
'theme' => 'afterburner',
'lang' => 'es',
]);
return [$coursecategory1, $course1, $coursecategory2, $course2];
}
/**
* Test callbacks are correctly applied for those columns using them
*/
public function test_get_columns_with_callbacks(): void {
$this->resetAfterTest();
[$coursecategory1, $course1] = $this->generate_courses();
$testdate = time();
// Add some customfields to the course.
$cfgenerator = self::getDataGenerator()->get_plugin_generator('core_customfield');
$params = [
'component' => 'core_course',
'area' => 'course',
'itemid' => 0,
'contextid' => context_system::instance()->id
];
$category = $cfgenerator->create_category($params);
$field1 = $cfgenerator->create_field(
['categoryid' => $category->get('id'), 'type' => 'text', 'name' => 'Customfield text 1', 'shortname' => 'cf1']);
$cfgenerator->add_instance_data($field1, (int)$course1->id, 'Do. Or do not. There is no try');
$field2 = $cfgenerator->create_field(
['categoryid' => $category->get('id'), 'type' => 'text', 'name' => 'Customfield text 2', 'shortname' => 'cf2']);
$cfgenerator->add_instance_data($field2, (int)$course1->id, 'Chewie, we are home');
$field3 = $cfgenerator->create_field(
['categoryid' => $category->get('id'), 'type' => 'checkbox', 'name' => 'Customfield checkbox', 'shortname' => 'cf3']);
$cfgenerator->add_instance_data($field3, (int)$course1->id, 1);
$field4 = $cfgenerator->create_field(
['categoryid' => $category->get('id'), 'type' => 'date', 'name' => 'Customfield date', 'shortname' => 'cf4']);
$cfgenerator->add_instance_data($field4, (int)$course1->id, $testdate);
$field5 = $cfgenerator->create_field(
['categoryid' => $category->get('id'), 'type' => 'select', 'name' => 'Customfield menu', 'shortname' => 'cf5',
'configdata' => ['defaultvalue' => 'Option A', 'options' => "Option A\nOption B\nOption C"]]);
// Select option C for course1 (options are counted starting from one).
$cfgenerator->add_instance_data($field5, (int)$course1->id, 3);
$tablerows = $this->get_report_table_rows();
$courserows = array_filter($tablerows, static function(array $row) use ($course1): bool {
return $row['shortname'] === $course1->shortname;
});
$courserow = reset($courserows);
$this->assertEquals($coursecategory1->name, $courserow['category']);
$this->assertEquals('Course 1', $courserow['fullname']);
$this->assertEquals('C1', $courserow['shortname']);
$this->assertEquals('IDNumber1', $courserow['idnumber']);
$this->assertEquals('Yes', $courserow['visible']);
$this->assertEquals(userdate(289308600), $courserow['startdate']);
$this->assertEquals(userdate(3445023600), $courserow['enddate']);
$this->assertEquals('No groups', $courserow['groupmode']);
$this->assertEquals('Yes', $courserow['enablecompletion']);
$this->assertEquals('No', $courserow['downloadcontent']);
$this->assertEquals('Topics format', $courserow['format']);
$this->assertEquals('Gregorian', $courserow['calendartype']);
$this->assertEquals('afterburner', $courserow['theme']);
$this->assertEquals(get_string_manager()->get_list_of_translations()['en'], $courserow['lang']);
$expected = '<a href="https://www.example.com/moodle/course/view.php?id=' . $course1->id . '">Course 1</a>';
$this->assertEquals($expected, $courserow['coursefullnamewithlink']);
$expected = '<a href="https://www.example.com/moodle/course/view.php?id=' . $course1->id . '">C1</a>';
$this->assertEquals($expected, $courserow['courseshortnamewithlink']);
$expected = '<a href="https://www.example.com/moodle/course/view.php?id=' . $course1->id . '">IDNumber1</a>';
$this->assertEquals($expected, $courserow['courseidnumberewithlink']);
$this->assertEquals('Do. Or do not. There is no try', $courserow['customfield_cf1']);
$this->assertEquals('Chewie, we are home', $courserow['customfield_cf2']);
$this->assertEquals('Yes', $courserow['customfield_cf3']);
$this->assertEquals(userdate($testdate), $courserow['customfield_cf4']);
$this->assertEquals('Option C', $courserow['customfield_cf5']);
}
/**
* Test filtering report by course fields
*/
public function test_filters(): void {
$this->resetAfterTest();
[$coursecategory1] = $this->generate_courses();
// Filter by fullname field.
$filtervalues = [
'course:fullname_operator' => text::IS_EQUAL_TO,
'course:fullname_value' => 'Course 1',
];
$tablerows = $this->get_report_table_rows($filtervalues);
$this->assertEquals([
'Course 1',
], array_column($tablerows, 'fullname'));
// Filter by shortname field.
$filtervalues = [
'course:shortname_operator' => text::IS_EQUAL_TO,
'course:shortname_value' => 'C1',
];
$tablerows = $this->get_report_table_rows($filtervalues);
$this->assertEquals([
'Course 1',
], array_column($tablerows, 'fullname'));
// Filter by idnumber field.
$filtervalues = [
'course:idnumber_operator' => text::IS_EQUAL_TO,
'course:idnumber_value' => 'IDNumber2',
];
$tablerows = $this->get_report_table_rows($filtervalues);
$this->assertEquals([
'Course 2',
], array_column($tablerows, 'fullname'));
// Filter by visible field.
$filtervalues = [
'course:visible_operator' => boolean_select::NOT_CHECKED,
];
$tablerows = $this->get_report_table_rows($filtervalues);
$this->assertEquals([
'Course 2',
], array_column($tablerows, 'fullname'));
// Filter by startdate field.
$filtervalues = [
'course:startdate_operator' => date::DATE_RANGE,
'course:startdate_from' => 289135800,
'course:startdate_to' => 289740600,
];
$tablerows = $this->get_report_table_rows($filtervalues);
$this->assertEquals([
'Course 1',
], array_column($tablerows, 'fullname'));
// Filter by category field.
$filtervalues = [
'course:category_operator' => select::EQUAL_TO,
'course:category_value' => $coursecategory1->id,
];
$tablerows = $this->get_report_table_rows($filtervalues);
$this->assertEquals([
'Course 1',
], array_column($tablerows, 'fullname'));
// Filter by group mode field.
$filtervalues = [
'course:groupmode_operator' => select::EQUAL_TO,
'course:groupmode_value' => SEPARATEGROUPS,
];
$tablerows = $this->get_report_table_rows($filtervalues);
$this->assertEquals([
'Course 2',
], array_column($tablerows, 'fullname'));
// Filter by enable completion field.
$filtervalues = [
'course:enablecompletion_operator' => boolean_select::CHECKED,
];
$tablerows = $this->get_report_table_rows($filtervalues);
$this->assertEquals([
'Course 1',
], array_column($tablerows, 'fullname'));
}
/**
* Test filtering report by course customfield
*/
public function test_customfield_text_filter(): void {
$this->resetAfterTest();
$course1 = $this->getDataGenerator()->create_course([
'fullname' => 'Philosophy and Superheroes',
'shortname' => 'PS1',
]);
$course2 = $this->getDataGenerator()->create_course([
'fullname' => 'The game of Mathematics',
'shortname' => 'GM1',
]);
// Add some customfields to the course.
$cfgenerator = self::getDataGenerator()->get_plugin_generator('core_customfield');
$params = [
'component' => 'core_course',
'area' => 'course',
'itemid' => 0,
'contextid' => context_system::instance()->id
];
$category = $cfgenerator->create_category($params);
$field = $cfgenerator->create_field(
['categoryid' => $category->get('id'), 'type' => 'text', 'name' => 'Customfield text 1', 'shortname' => 'cf']);
$cfgenerator->add_instance_data($field, (int)$course1->id, 'Leia Organa');
$cfgenerator->add_instance_data($field, (int)$course2->id, 'Han Solo');
$field5 = $cfgenerator->create_field(
['categoryid' => $category->get('id'), 'type' => 'select', 'name' => 'Customfield menu', 'shortname' => 'cf5',
'configdata' => ['defaultvalue' => 'Option A', 'options' => "Option A\nOption B\nOption C"]]);
$cfgenerator->add_instance_data($field5, (int)$course1->id, 3);
$cfgenerator->add_instance_data($field5, (int)$course2->id, 2);
$filtervalues = [
'course:customfield_cf_operator' => text::IS_EQUAL_TO,
'course:customfield_cf_value' => 'Leia Organa',
];
$tablerows = $this->get_report_table_rows($filtervalues);
$this->assertEquals([
'Philosophy and Superheroes',
], array_column($tablerows, 'fullname'));
$filtervalues = [
'course:customfield_cf_operator' => text::IS_EQUAL_TO,
'course:customfield_cf_value' => 'Han Solo',
];
$tablerows = $this->get_report_table_rows($filtervalues);
$this->assertEquals([
'The game of Mathematics',
], array_column($tablerows, 'fullname'));
// Filter by menu customfield.
$filtervalues = [
'course:customfield_cf5_operator' => select::EQUAL_TO,
'course:customfield_cf5_value' => 3, // Option C.
];
$tablerows = $this->get_report_table_rows($filtervalues);
$this->assertEquals([
'Philosophy and Superheroes',
], array_column($tablerows, 'fullname'));
// Filter by course customfield that doesn't exist.
$filtervalues = [
'course:customfield_cf_operator' => text::IS_EQUAL_TO,
'course:customfield_cf_value' => 'Luke Skywalker',
];
$tablerows = $this->get_report_table_rows($filtervalues);
$this->assertEmpty($tablerows);
}
/**
* Helper method to create the report, and return it's rows
*
* @param array $filtervalues
* @return array
*/
private function get_report_table_rows(array $filtervalues = []): array {
$report = manager::create_report_persistent((object) [
'type' => course_entity_report::TYPE_SYSTEM_REPORT,
'source' => course_entity_report::class,
]);
user_filter_manager::set($report->get('id'), $filtervalues);
return testable_system_report_table::create($report->get('id'), [])->get_table_rows();
}
/**
* Test entity table alias
*/
public function test_table_alias(): void {
$courseentity = new course();
$this->assertEquals('c', $courseentity->get_table_alias('course'));
$courseentity->set_table_alias('course', 'newalias');
$this->assertEquals('newalias', $courseentity->get_table_alias('course'));
}
/**
* Test for invalid get table alias
*/
public function test_get_table_alias_invalid(): void {
$courseentity = new course();
$this->expectException(coding_exception::class);
$this->expectExceptionMessage('Coding error detected, it must be fixed by a programmer: ' .
'Invalid table name (nonexistingalias)');
$courseentity->get_table_alias('nonexistingalias');
}
/**
* Test invalid entity set table alias
*/
public function test_set_table_alias_invalid(): void {
$courseentity = new course();
$this->expectException(coding_exception::class);
$this->expectExceptionMessage('Coding error detected, it must be fixed by a programmer: Invalid table name (nonexistent)');
$courseentity->set_table_alias('nonexistent', 'newalias');
}
/**
* Test entity name
*/
public function test_name(): void {
$courseentity = new course();
$this->assertEquals('course', $courseentity->get_entity_name());
$courseentity->set_entity_name('newentityname');
$this->assertEquals('newentityname', $courseentity->get_entity_name());
}
/**
* Test invalid entity name
*/
public function test_name_invalid(): void {
$courseentity = new course();
$this->expectException(coding_exception::class);
$this->expectExceptionMessage('Entity name must be comprised of alphanumeric character, underscore or dash');
$courseentity->set_entity_name('');
}
/**
* Test entity title
*/
public function test_title(): void {
$courseentity = new course();
$this->assertEquals(new lang_string('entitycourse', 'core_reportbuilder'), $courseentity->get_entity_title());
$newtitle = new lang_string('fullname');
$courseentity->set_entity_title($newtitle);
$this->assertEquals($newtitle, $courseentity->get_entity_title());
}
/**
* Test adding single join
*/
public function test_add_join(): void {
$courseentity = (new course())
->set_table_alias('course', 'c1');
$tablejoin = "JOIN {course} c2 ON c2.id = c1.id";
$courseentity->add_join($tablejoin);
$method = (new ReflectionClass(course::class))->getMethod('get_joins');
$method->setAccessible(true);
$this->assertEquals([$tablejoin], $method->invoke($courseentity));
}
/**
* Test adding multiple join
*/
public function test_add_joins(): void {
$courseentity = (new course())
->set_table_alias('course', 'c1');
$tablejoins = [
"JOIN {course} c2 ON c2.id = c1.id",
"JOIN {course} c3 ON c3.id = c1.id",
];
$courseentity->add_joins($tablejoins);
$method = (new ReflectionClass(course::class))->getMethod('get_joins');
$method->setAccessible(true);
$this->assertEquals($tablejoins, $method->invoke($courseentity));
}
/**
* Test for invalid get column
*/
public function test_get_column_invalid(): void {
$courseentity = new course();
$this->expectException(coding_exception::class);
$this->expectExceptionMessage('Coding error detected, it must be fixed by a programmer: ' .
'Invalid column name (nonexistingcolumn)');
$courseentity->get_column('nonexistingcolumn');
}
/**
* Test for invalid get filter
*/
public function test_get_filter_invalid(): void {
$courseentity = new course();
$this->expectException(coding_exception::class);
$this->expectExceptionMessage('Coding error detected, it must be fixed by a programmer: ' .
'Invalid filter name (nonexistingfilter)');
$courseentity->get_filter('nonexistingfilter');
}
}
@@ -0,0 +1,290 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\local\entities;
use advanced_testcase;
use core_reportbuilder\local\filters\boolean_select;
use core_reportbuilder\local\filters\date;
use moodle_url;
use core_reportbuilder\manager;
use core_reportbuilder\testable_system_report_table;
use core_reportbuilder\user_entity_report;
use core_reportbuilder\local\filters\text;
use core_reportbuilder\local\helpers\user_filter_manager;
/**
* Unit tests for user entity
*
* @package core_reportbuilder
* @covers \core_reportbuilder\local\entities\base
* @covers \core_reportbuilder\local\entities\user
* @covers \core_reportbuilder\local\helpers\user_profile_fields
* @covers \core_reportbuilder\local\report\base
* @covers \core_reportbuilder\system_report
* @copyright 2021 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class user_testcase extends advanced_testcase {
/**
* Load required classes
*/
public static function setUpBeforeClass(): void {
global $CFG;
require_once("{$CFG->dirroot}/reportbuilder/tests/fixtures/testable_system_report_table.php");
require_once("{$CFG->dirroot}/reportbuilder/tests/fixtures/user_entity_report.php");
require_once("{$CFG->dirroot}/user/profile/lib.php");
}
/**
* Test callbacks are correctly applied for those columns using them
*/
public function test_columns_with_callbacks(): void {
$this->resetAfterTest();
// Add a couple of user profile fields to show on the report.
$this->getDataGenerator()->create_custom_profile_field(['datatype' => 'text',
'shortname' => 'favcolor', 'name' => 'Favorite color']);
$this->getDataGenerator()->create_custom_profile_field(['datatype' => 'text',
'shortname' => 'favsuperpower', 'name' => 'Favorite super power']);
$user = $this->getDataGenerator()->create_user([
'suspended' => 1,
'confirmed' => 0,
'country' => 'ES',
'profile_field_favcolor' => 'Blue',
'profile_field_favsuperpower' => 'Time travel',
]);
$tablerows = $this->get_report_table_rows();
$userrows = array_filter($tablerows, static function(array $row) use ($user): bool {
return $row['username'] === $user->username;
});
$userrow = reset($userrows);
$this->assertEquals('Yes', $userrow['suspended']);
$this->assertEquals('No', $userrow['confirmed']);
$this->assertEquals('Spain', $userrow['country']);
$this->assertEquals('Blue', $userrow['profilefield_favcolor']);
$this->assertEquals('Time travel', $userrow['profilefield_favsuperpower']);
}
/**
* Test the formatted user fullname columns
*/
public function test_fullname_columns(): void {
global $OUTPUT;
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user([]);
$tablerows = $this->get_report_table_rows();
$userrows = array_filter($tablerows, static function(array $row) use ($user): bool {
return $row['username'] === $user->username;
});
$userrow = reset($userrows);
$userfullname = fullname($user);
$userprofile = (new moodle_url('/user/profile.php', ['id' => $user->id]))->out();
$userpicture = $OUTPUT->user_picture($user, ['link' => false, 'alttext' => false]);
$this->assertEquals($userfullname, $userrow['fullname']);
$this->assertEquals('<a href="' . $userprofile . '">' . $userfullname . '</a>', $userrow['fullnamewithlink']);
$this->assertEquals($userpicture . $userfullname, $userrow['fullnamewithpicture']);
$this->assertEquals('<a href="' . $userprofile . '">' . $userpicture . $userfullname . '</a>',
$userrow['fullnamewithpicturelink']);
}
/**
* Test picture column callback
*/
public function test_picture_column(): void {
global $OUTPUT;
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user([]);
$tablerows = $this->get_report_table_rows();
$userrows = array_filter($tablerows, static function(array $row) use ($user): bool {
return $row['username'] === $user->username;
});
$userrow = reset($userrows);
$userpicture = $OUTPUT->user_picture($user, ['link' => false, 'alttext' => false]);
$this->assertEquals($userpicture, $userrow['picture']);
}
/**
* Test filtering report by user fields
*/
public function test_filters(): void {
$this->resetAfterTest();
$this->getDataGenerator()->create_user(['firstname' => 'Daffy', 'lastname' => 'Duck', 'email' => '[email protected]',
'city' => 'LA', 'lastaccess' => time() - YEARSECS, 'suspended' => 1]);
$this->getDataGenerator()->create_user(['firstname' => 'Donald', 'lastname' => 'Duck', 'email' => '[email protected]',
'city' => 'Chicago', 'lastaccess' => time(), 'suspended' => 0]);
// Filter by fullname field.
$filtervalues = [
'user:fullname_operator' => text::IS_EQUAL_TO,
'user:fullname_value' => 'Daffy Duck',
];
$tablerows = $this->get_report_table_rows($filtervalues);
$this->assertEquals([
'Daffy Duck',
], array_column($tablerows, 'fullname'));
// Filter by firstname field.
$filtervalues = [
'user:firstname_operator' => text::CONTAINS,
'user:firstname_value' => 'Donald',
];
$tablerows = $this->get_report_table_rows($filtervalues);
$this->assertEquals([
'Donald Duck',
], array_column($tablerows, 'fullname'));
// Filter by lastname field.
$filtervalues = [
'user:lastname_operator' => text::CONTAINS,
'user:lastname_value' => 'Duck',
];
$tablerows = $this->get_report_table_rows($filtervalues);
$this->assertEqualsCanonicalizing([
'Donald Duck',
'Daffy Duck',
], array_column($tablerows, 'fullname'));
// Filter by email field.
$filtervalues = [
'user:email_operator' => text::IS_EQUAL_TO,
'user:email_value' => '[email protected]',
];
$tablerows = $this->get_report_table_rows($filtervalues);
$this->assertEquals([
'Donald Duck',
], array_column($tablerows, 'fullname'));
// Filter by city field.
$filtervalues = [
'user:city_operator' => text::IS_EQUAL_TO,
'user:city_value' => 'Chicago',
];
$tablerows = $this->get_report_table_rows($filtervalues);
$this->assertEquals([
'Donald Duck',
], array_column($tablerows, 'fullname'));
// Filter by city field.
$filtervalues = [
'user:city_operator' => text::IS_EQUAL_TO,
'user:city_value' => 'Chicago',
];
$tablerows = $this->get_report_table_rows($filtervalues);
$this->assertEquals([
'Donald Duck',
], array_column($tablerows, 'fullname'));
// Filter by lastaccess field.
$filtervalues = [
'user:lastaccess_operator' => date::DATE_RANGE,
'user:lastaccess_from' => time() - YEARSECS - 100,
'user:lastaccess_to' => time() - YEARSECS + 100,
];
$tablerows = $this->get_report_table_rows($filtervalues);
$this->assertEquals([
'Daffy Duck',
], array_column($tablerows, 'fullname'));
// Filter by suspened field.
$filtervalues = [
'user:suspended_operator' => boolean_select::CHECKED,
];
$tablerows = $this->get_report_table_rows($filtervalues);
$this->assertEquals([
'Daffy Duck',
], array_column($tablerows, 'fullname'));
}
/**
* Test filtering report by a user profile field
*/
public function test_userprofilefield_filter(): void {
$this->resetAfterTest();
// Add a user profile field to show on the report.
$this->getDataGenerator()->create_custom_profile_field(['datatype' => 'text',
'shortname' => 'favcolor', 'name' => 'Favorite color']);
$this->getDataGenerator()->create_user(['firstname' => 'Daffy', 'lastname' => 'Duck', 'profile_field_favcolor' => 'Blue']);
$this->getDataGenerator()->create_user(['firstname' => 'Donald', 'lastname' => 'Duck',
'profile_field_favcolor' => 'Green']);
$filtervalues = [
'user:profilefield_favcolor_operator' => text::IS_EQUAL_TO,
'user:profilefield_favcolor_value' => 'Green',
];
$tablerows = $this->get_report_table_rows($filtervalues);
$this->assertEquals([
'Donald Duck',
], array_column($tablerows, 'fullname'));
}
/**
* Tests the helper method for selecting all of a users' name fields
*/
public function test_get_name_fields_select(): void {
global $DB;
$fields = user::get_name_fields_select('u');
$user = $DB->get_record_sql("SELECT {$fields} FROM {user} u WHERE username = :username", ['username' => 'admin']);
// Ensure we received back all name fields.
$this->assertEqualsCanonicalizing([
'firstname',
'lastname',
'firstnamephonetic',
'lastnamephonetic',
'middlename',
'alternatename',
], array_keys((array) $user));
}
/**
* Helper method to create the report, and return it's rows
*
* @param array $filtervalues
* @return array
*/
private function get_report_table_rows(array $filtervalues = []): array {
$report = manager::create_report_persistent((object) [
'type' => user_entity_report::TYPE_SYSTEM_REPORT,
'source' => user_entity_report::class,
]);
user_filter_manager::set($report->get('id'), $filtervalues);
return testable_system_report_table::create($report->get('id'), [])->get_table_rows();
}
}
@@ -0,0 +1,86 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\local\filters;
use advanced_testcase;
use lang_string;
use core_reportbuilder\local\report\filter;
/**
* Unit tests for boolean report filter
*
* @package core_reportbuilder
* @covers \core_reportbuilder\local\filters\base
* @covers \core_reportbuilder\local\filters\boolean_select
* @copyright 2021 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class boolean_select_testcase extends advanced_testcase {
/**
* Data provider for {@see test_get_sql_filter_simple}
*
* @return array
*/
public function get_sql_filter_simple_provider(): array {
return [
[boolean_select::ANY_VALUE, true],
[boolean_select::CHECKED, true],
[boolean_select::NOT_CHECKED, false],
];
}
/**
* Test getting filter SQL
*
* @param int $operator
* @param bool $expectuser
*
* @dataProvider get_sql_filter_simple_provider
*/
public function test_get_sql_filter_simple(int $operator, bool $expectuser): void {
global $DB;
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user([
'suspended' => 1,
]);
$filter = new filter(
boolean_select::class,
'test',
new lang_string('yes'),
'testentity',
'suspended'
);
// Create instance of our filter, passing given operator.
[$select, $params] = boolean_select::create($filter)->get_sql_filter([
$filter->get_unique_identifier() . '_operator' => $operator,
]);
$usernames = $DB->get_fieldset_select('user', 'username', $select, $params);
if ($expectuser) {
$this->assertContains($user->username, $usernames);
} else {
$this->assertNotContains($user->username, $usernames);
}
}
}
@@ -0,0 +1,77 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\local\filters;
use advanced_testcase;
use core_reportbuilder\local\report\filter;
/**
* Unit tests for course selector filter
*
* @package core_reportbuilder
* @covers \core_reportbuilder\local\filters\base
* @covers \core_reportbuilder\local\filters\course_selector
* @copyright 2021 David Matamoros <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_selector_testcase extends advanced_testcase {
/**
* Test getting filter SQL
*/
public function test_get_sql_filter(): void {
global $DB;
$this->resetAfterTest();
$course1 = $this->getDataGenerator()->create_course([
'fullname' => "Time travel",
]);
$course2 = $this->getDataGenerator()->create_course([
'fullname' => "Quantum computing",
]);
$course3 = $this->getDataGenerator()->create_course([
'fullname' => "Space travel",
]);
$filter = new filter(
course_selector::class,
'test',
new \lang_string('course'),
'testentity',
'id'
);
// Create instance of our filter, passing given courses ids.
[$select, $params] = text::create($filter)->get_sql_filter([
$filter->get_unique_identifier() . '_values' => [$course1->id, $course3->id],
]);
$fullnames = $DB->get_fieldset_select('course', 'fullname', $select, $params);
$this->assertEqualsCanonicalizing(['Time travel', 'Space travel'], $fullnames);
// Test without passing any course id.
[$select, $params] = text::create($filter)->get_sql_filter([
$filter->get_unique_identifier() . '_values' => [],
]);
$fullnames = $DB->get_fieldset_select('course', 'fullname', $select, $params);
$this->assertEqualsCanonicalizing(['Time travel', 'Quantum computing', 'Space travel', 'PHPUnit test site'], $fullnames);
}
}
@@ -0,0 +1,117 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\local\filters;
use advanced_testcase;
use lang_string;
use core_reportbuilder\local\report\filter;
/**
* Unit tests for date report filter
*
* @package core_reportbuilder
* @covers \core_reportbuilder\local\filters\base
* @covers \core_reportbuilder\local\filters\date
* @copyright 2021 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class date_testcase extends advanced_testcase {
/**
* Data provider for {@see test_get_sql_filter_simple}
*
* @return array
*/
public function get_sql_filter_simple_provider(): array {
return [
[date::DATE_ANY, true],
[date::DATE_NOT_EMPTY, true],
[date::DATE_EMPTY, false],
];
}
/**
* Test getting filter SQL
*
* @param int $operator
* @param bool $expectuser
*
* @dataProvider get_sql_filter_simple_provider
*/
public function test_get_sql_filter_simple(int $operator, bool $expectuser): void {
global $DB;
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user([
'timecreated' => 12345,
]);
$filter = new filter(
date::class,
'test',
new lang_string('yes'),
'testentity',
'timecreated'
);
// Create instance of our filter, passing given operator.
[$select, $params] = date::create($filter)->get_sql_filter([
$filter->get_unique_identifier() . '_operator' => $operator,
]);
$usernames = $DB->get_fieldset_select('user', 'username', $select, $params);
if ($expectuser) {
$this->assertContains($user->username, $usernames);
} else {
$this->assertNotContains($user->username, $usernames);
}
}
/**
* Test getting filter SQL while specifying a date range
*/
public function test_get_sql_filter_date_range(): void {
global $DB;
$this->resetAfterTest();
$userone = $this->getDataGenerator()->create_user(['timecreated' => 50]);
$usertwo = $this->getDataGenerator()->create_user(['timecreated' => 100]);
$filter = new filter(
date::class,
'test',
new lang_string('yes'),
'testentity',
'timecreated'
);
// Create instance of our date range filter.
[$select, $params] = date::create($filter)->get_sql_filter([
$filter->get_unique_identifier() . '_operator' => date::DATE_RANGE,
$filter->get_unique_identifier() . '_from' => 80,
$filter->get_unique_identifier() . '_to' => 120,
]);
// The only matching user should be our first test user.
$usernames = $DB->get_fieldset_select('user', 'username', $select, $params);
$this->assertEquals([$usertwo->username], $usernames);
}
}
@@ -0,0 +1,148 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\local\filters;
use advanced_testcase;
use lang_string;
use core_reportbuilder\local\report\filter;
/**
* Unit tests for number report filter
*
* @package core_reportbuilder
* @covers \core_reportbuilder\local\filters\base
* @covers \core_reportbuilder\local\filters\number
* @copyright 2021 David Matamoros <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class number_testcase extends advanced_testcase {
/**
* Data provider for {@see test_get_sql_filter_simple}
*
* @return array[]
*/
public function get_sql_filter_simple_provider(): array {
return [
[number::ANY_VALUE, null, null, true],
[number::IS_NOT_EMPTY, null, null, true],
[number::IS_EMPTY, null, null, false],
[number::LESS_THAN, 1, null, false],
[number::LESS_THAN, 123, null, false],
[number::LESS_THAN, 124, null, true],
[number::GREATER_THAN, 1, null, true],
[number::GREATER_THAN, 123, null, false],
[number::GREATER_THAN, 124, null, false],
[number::EQUAL_TO, 123, null, true],
[number::EQUAL_TO, 124, null, false],
[number::EQUAL_OR_LESS_THAN, 124, null, true],
[number::EQUAL_OR_LESS_THAN, 123, null, true],
[number::EQUAL_OR_LESS_THAN, 122, null, false],
[number::EQUAL_OR_GREATER_THAN, 122, null, true],
[number::EQUAL_OR_GREATER_THAN, 123, null, true],
[number::EQUAL_OR_GREATER_THAN, 124, null, false],
[number::RANGE, 122, 124, true],
[number::RANGE, 124, 125, false],
[number::RANGE, 122, 123, true],
[number::RANGE, 123, 124, true],
];
}
/**
* Test getting filter SQL
*
* @param int $operator
* @param int|null $value1
* @param int|null $value2
* @param bool $expectmatch
*
* @dataProvider get_sql_filter_simple_provider
*/
public function test_get_sql_filter_simple(int $operator, ?int $value1, ?int $value2, bool $expectmatch): void {
global $DB;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course([
'timecreated' => 123,
]);
$filter = new filter(
number::class,
'test',
new lang_string('course'),
'testentity',
'timecreated'
);
// Create instance of our filter, passing given operator.
[$select, $params] = number::create($filter)->get_sql_filter([
$filter->get_unique_identifier() . '_value1' => $value1,
$filter->get_unique_identifier() . '_value2' => $value2,
$filter->get_unique_identifier() . '_operator' => $operator,
]);
$fullnames = $DB->get_fieldset_select('course', 'fullname', $select, $params);
if ($expectmatch) {
$this->assertContains($course->fullname, $fullnames);
} else {
$this->assertNotContains($course->fullname, $fullnames);
}
}
/**
* Data provider for {@see test_get_sql_filter_invalid}
*
* @return array[]
*/
public function get_sql_filter_invalid_provider(): array {
return [
[number::LESS_THAN],
[number::GREATER_THAN],
[number::EQUAL_TO],
[number::EQUAL_OR_LESS_THAN],
[number::EQUAL_OR_GREATER_THAN],
[number::RANGE],
];
}
/**
* Test getting filter SQL for operators that require values
*
* @param int $operator
*
* @dataProvider get_sql_filter_invalid_provider
*/
public function test_get_sql_filter_invalid(int $operator): void {
$filter = new filter(
number::class,
'test',
new lang_string('course'),
'testentity',
'timecreated'
);
[$select, $params] = number::create($filter)->get_sql_filter([
$filter->get_unique_identifier() . '_operator' => $operator,
]);
$this->assertEquals('', $select);
$this->assertEquals([], $params);
}
}
@@ -0,0 +1,98 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\local\filters;
use advanced_testcase;
use lang_string;
use core_reportbuilder\local\report\filter;
/**
* Unit tests for select report filter
*
* @package core_reportbuilder
* @covers \core_reportbuilder\local\filters\base
* @covers \core_reportbuilder\local\filters\select
* @copyright 2021 David Matamoros <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class select_testcase extends advanced_testcase {
/**
* Data provider for {@see test_get_sql_filter_simple}
*
* @return array
*/
public function get_sql_filter_simple_provider(): array {
return [
[select::ANY_VALUE, null, true],
[select::EQUAL_TO, 'starwars', true],
[select::EQUAL_TO, 'mandalorian', false],
[select::NOT_EQUAL_TO, 'starwars', false],
[select::NOT_EQUAL_TO, 'mandalorian', true],
];
}
/**
* Test getting filter SQL
*
* @param int $operator
* @param string|null $value
* @param bool $expectmatch
*
* @dataProvider get_sql_filter_simple_provider
*/
public function test_get_sql_filter_simple(int $operator, ?string $value, bool $expectmatch): void {
global $DB;
$this->resetAfterTest();
$course1 = $this->getDataGenerator()->create_course([
'fullname' => "May the course be with you",
'shortname' => 'starwars',
]);
$course2 = $this->getDataGenerator()->create_course([
'fullname' => "This is the course",
'shortname' => 'mandalorian',
]);
$filter = (new filter(
select::class,
'test',
new lang_string('course'),
'testentity',
'shortname'
))->set_options([
$course1->shortname => $course1->fullname,
$course2->shortname => $course2->fullname,
]);
// Create instance of our filter, passing given operator.
[$select, $params] = select::create($filter)->get_sql_filter([
$filter->get_unique_identifier() . '_operator' => $operator,
$filter->get_unique_identifier() . '_value' => $value,
]);
$fullnames = $DB->get_fieldset_select('course', 'fullname', $select, $params);
if ($expectmatch) {
$this->assertContains($course1->fullname, $fullnames);
} else {
$this->assertNotContains($course1->fullname, $fullnames);
}
}
}
@@ -0,0 +1,100 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\local\filters;
use advanced_testcase;
use lang_string;
use core_reportbuilder\local\report\filter;
/**
* Unit tests for text report filter
*
* @package core_reportbuilder
* @covers \core_reportbuilder\local\filters\base
* @covers \core_reportbuilder\local\filters\text
* @copyright 2021 David Matamoros <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class text_testcase extends advanced_testcase {
/**
* Data provider for {@see test_get_sql_filter_simple}
*
* @return array
*/
public function get_sql_filter_simple_provider(): array {
return [
[text::ANY_VALUE, null, true],
[text::CONTAINS, 'looking', true],
[text::CONTAINS, 'sky', false],
[text::DOES_NOT_CONTAIN, 'sky', true],
[text::DOES_NOT_CONTAIN, 'looking', false],
[text::IS_EQUAL_TO, "Hello, is it me you're looking for?", true],
[text::IS_EQUAL_TO, 'I can see it in your eyes', false],
[text::IS_NOT_EQUAL_TO, "Hello, is it me you're looking for?", false],
[text::IS_NOT_EQUAL_TO, 'I can see it in your eyes', true],
[text::STARTS_WITH, 'Hello', true],
[text::STARTS_WITH, 'sunlight', false],
[text::ENDS_WITH, 'looking for?', true],
[text::ENDS_WITH, 'your heart', false],
[text::IS_EMPTY, null, false],
[text::IS_NOT_EMPTY, null, true],
];
}
/**
* Test getting filter SQL
*
* @param int $operator
* @param string|null $value
* @param bool $expectmatch
*
* @dataProvider get_sql_filter_simple_provider
*/
public function test_get_sql_filter_simple(int $operator, ?string $value, bool $expectmatch): void {
global $DB;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course([
'fullname' => "Hello, is it me you're looking for?",
]);
$filter = new filter(
text::class,
'test',
new lang_string('course'),
'testentity',
'fullname'
);
// Create instance of our filter, passing given operator.
[$select, $params] = text::create($filter)->get_sql_filter([
$filter->get_unique_identifier() . '_operator' => $operator,
$filter->get_unique_identifier() . '_value' => $value,
]);
$fullnames = $DB->get_fieldset_select('course', 'fullname', $select, $params);
if ($expectmatch) {
$this->assertContains($course->fullname, $fullnames);
} else {
$this->assertNotContains($course->fullname, $fullnames);
}
}
}
@@ -0,0 +1,133 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\local\helpers;
use advanced_testcase;
use core_reportbuilder\local\entities\course;
use core_reportbuilder\local\report\column;
use core_reportbuilder\local\report\filter;
/**
* Unit tests for custom fields helper
*
* @package core_reportbuilder
* @covers \core_reportbuilder\local\helpers\custom_fields
* @copyright 2021 David Matamoros <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class custom_fields_testcase extends advanced_testcase {
/**
* Generate a course with customfields
*/
public function generate_course_with_customfields(): custom_fields {
$course = $this->getDataGenerator()->create_course();
// Add some customfields to the course.
$cfgenerator = self::getDataGenerator()->get_plugin_generator('core_customfield');
$params = [
'component' => 'core_course',
'area' => 'course',
'itemid' => 0,
'contextid' => \context_system::instance()->id
];
$category = $cfgenerator->create_category($params);
$field1 = $cfgenerator->create_field(
['categoryid' => $category->get('id'), 'type' => 'text', 'name' => 'Customfield text 1', 'shortname' => 'cf1']);
$cfgenerator->add_instance_data($field1, (int)$course->id, 'C-3PO');
$field2 = $cfgenerator->create_field(
['categoryid' => $category->get('id'), 'type' => 'text', 'name' => 'Customfield text 2', 'shortname' => 'cf2']);
$cfgenerator->add_instance_data($field2, (int)$course->id, 'R2-D2');
$courseentity = new course();
$coursealias = $courseentity->get_table_alias('course');
// Create an instance of the customfields helper.
return new custom_fields($coursealias . '.id', $courseentity->get_entity_name(),
'core_course', 'course');
}
/**
* Test for get_columns
*/
public function test_get_columns(): void {
$this->resetAfterTest();
$customfields = $this->generate_course_with_customfields();
$columns = $customfields->get_columns();
$this->assertCount(2, $columns);
[$column0, $column1] = $columns;
$this->assertInstanceOf(column::class, $column0);
$this->assertInstanceOf(column::class, $column1);
$this->assertEqualsCanonicalizing(['Customfield text 1', 'Customfield text 2'],
[$column0->get_title(), $column1->get_title()]);
$this->assertEquals(column::TYPE_TEXT, $column0->get_type());
$this->assertEquals('course', $column0->get_entity_name());
$this->assertStringStartsWith('LEFT JOIN {customfield_data}', $column0->get_joins()[0]);
// Column of type TEXT is sortable.
$this->assertTrue($column0->get_is_sortable());
}
/**
* Test for add_join
*/
public function test_add_join(): void {
$this->resetAfterTest();
$customfields = $this->generate_course_with_customfields();
$columns = $customfields->get_columns();
$this->assertCount(1, ($columns[0])->get_joins());
$customfields->add_join('JOIN {test} t ON t.id = id');
$columns = $customfields->get_columns();
$this->assertCount(2, ($columns[0])->get_joins());
}
/**
* Test for add_joins
*/
public function test_add_joins(): void {
$this->resetAfterTest();
$customfields = $this->generate_course_with_customfields();
$columns = $customfields->get_columns();
$this->assertCount(1, ($columns[0])->get_joins());
$customfields->add_joins(['JOIN {test} t ON t.id = id', 'JOIN {test2} t2 ON t2.id = id']);
$columns = $customfields->get_columns();
$this->assertCount(3, ($columns[0])->get_joins());
}
/**
* Test for get_filters
*/
public function test_get_filters(): void {
$this->resetAfterTest();
$customfields = $this->generate_course_with_customfields();
$filters = $customfields->get_filters();
$this->assertCount(2, $filters);
[$filter0, $filter1] = $filters;
$this->assertInstanceOf(filter::class, $filter0);
$this->assertInstanceOf(filter::class, $filter1);
$this->assertEqualsCanonicalizing(['Customfield text 1', 'Customfield text 2'],
[$filter0->get_header(), $filter1->get_header()]);
}
}
@@ -0,0 +1,81 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\local\helpers;
use advanced_testcase;
use coding_exception;
use core_user;
/**
* Unit tests for the database helper class
*
* @package core_reportbuilder
* @covers \core_reportbuilder\local\helpers\database
* @copyright 2020 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class database_testcase extends advanced_testcase {
/**
* Test generating table alias and parameter names
*/
public function test_generate_alias_params(): void {
global $DB;
$admin = core_user::get_user_by_username('admin');
$usertablealias = database::generate_alias();
$usertablealiasjoin = database::generate_alias();
$useridalias = database::generate_alias();
$paramuserid = database::generate_param_name();
$paramuserdeleted = database::generate_param_name();
// Ensure they are different.
$this->assertNotEquals($usertablealias, $usertablealiasjoin);
$this->assertNotEquals($paramuserid, $paramuserdeleted);
$sql = "SELECT {$usertablealias}.id AS {$useridalias}
FROM {user} {$usertablealias}
JOIN {user} {$usertablealiasjoin} ON {$usertablealiasjoin}.id = {$usertablealias}.id
WHERE {$usertablealias}.id = :{$paramuserid} AND {$usertablealias}.deleted = :{$paramuserdeleted}";
$params = [$paramuserid => $admin->id, $paramuserdeleted => 0];
$validated = database::validate_params($params);
$this->assertTrue($validated);
$record = $DB->get_record_sql($sql, $params);
$this->assertEquals($admin->id, $record->{$useridalias});
}
/**
* Test parameter validation
*/
public function test_validate_params(): void {
$params = [
database::generate_param_name() => 1,
'invalidfoo' => 2,
'invalidbar' => 4,
];
$this->expectException(coding_exception::class);
$this->expectExceptionMessage('Invalid parameter names (invalidfoo, invalidbar)');
database::validate_params($params);
}
}
@@ -0,0 +1,67 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\local\helpers;
use advanced_testcase;
use stdClass;
/**
* Unit tests for the format helper
*
* @package core_reportbuilder
* @covers \core_reportbuilder\local\helpers\format
* @copyright 2021 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class format_testcase extends advanced_testcase {
/**
* Test userdate method
*/
public function test_userdate(): void {
$now = time();
$userdate = format::userdate($now, new stdClass());
$this->assertEquals(userdate($now), $userdate);
}
/**
* Data provider for {@see test_boolean_as_text}
*
* @return array
*/
public function boolean_as_text_provider(): array {
return [
[false, get_string('no')],
[true, get_string('yes')],
];
}
/**
* Test boolean as text
*
* @param bool $value
* @param string $expected
*
* @dataProvider boolean_as_text_provider
*/
public function test_boolean_as_text(bool $value, string $expected): void {
$this->assertEquals($expected, format::boolean_as_text($value));
}
}
@@ -0,0 +1,233 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\local\helpers;
use advanced_testcase;
/**
* Unit tests for the user filter helper
*
* @package core_reportbuilder
* @covers \core_reportbuilder\local\helpers\user_filter_manager
* @copyright 2021 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class user_filter_manager_testcase extends advanced_testcase {
/**
* Helper method to return all user preferences for filters - based on the current storage backend using the same
*
* @return array
*/
private function get_filter_preferences(): array {
return array_filter(get_user_preferences(), static function(string $key): bool {
return strpos($key, 'reportbuilder-report-') === 0;
}, ARRAY_FILTER_USE_KEY);
}
/**
* Data provider for {@see test_get}
*
* @return array
*/
public function get_provider(): array {
return [
'Small value' => ['foo'],
'Large value' => [str_repeat('A', 4000)],
'Empty value' => [''],
];
}
/**
* Test getting filter values
*
* @param string $value
*
* @dataProvider get_provider
*/
public function test_get(string $value): void {
$this->resetAfterTest();
$values = [
'entity:filter_name' => $value,
];
user_filter_manager::set(5, $values);
// Make sure we get the same value back.
$this->assertEquals($values, user_filter_manager::get(5));
}
/**
* Test getting filter values that once spanned multiple chunks
*/
public function test_get_large_to_small(): void {
$this->resetAfterTest();
// Set a large initial filter value.
user_filter_manager::set(5, [
'longvalue' => str_repeat('ABCD', 1000),
]);
// Sanity check, there should be 4 (because 4000 characters plus some JSON encoding requires that many chunks).
$preferences = $this->get_filter_preferences();
$this->assertCount(4, $preferences);
$values = [
'longvalue' => 'ABCD',
];
user_filter_manager::set(5, $values);
// Make sure we get the same value back.
$this->assertEquals($values, user_filter_manager::get(5));
// Everything should now fit in a single filter preference.
$preferences = $this->get_filter_preferences();
$this->assertCount(1, $preferences);
}
/**
* Test getting filter values that haven't been set
*/
public function test_get_empty(): void {
$this->assertEquals([], user_filter_manager::get(5));
}
/**
* Data provider for {@see test_reset_all}
*
* @return array
*/
public function reset_all_provider(): array {
return [
'Small value' => ['foo'],
'Large value' => [str_repeat('A', 4000)],
'Empty value' => [''],
];
}
/**
* Test resetting all filter values
*
* @param string $value
*
* @dataProvider reset_all_provider
*/
public function test_reset_all(string $value): void {
$this->resetAfterTest();
user_filter_manager::set(5, [
'entity:filter_name' => $value
]);
$reset = user_filter_manager::reset_all(5);
$this->assertTrue($reset);
// We should get an empty array back.
$this->assertEquals([], user_filter_manager::get(5));
// All filter preferences should be removed.
$this->assertEmpty($this->get_filter_preferences());
}
/**
* Test resetting single filter values
*/
public function test_reset_single(): void {
$this->resetAfterTest();
user_filter_manager::set(5, [
'entity:filter_name' => 'foo',
'entity:filter_value' => 'bar',
'entity:other_name' => 'baz',
'entity:other_value' => 'bax',
]);
$reset = user_filter_manager::reset_single(5, 'entity:other');
$this->assertTrue($reset);
$this->assertEquals([
'entity:filter_name' => 'foo',
'entity:filter_value' => 'bar',
], user_filter_manager::get(5));
}
/**
* Test merging filter values
*/
public function test_merge(): void {
$this->resetAfterTest();
$values = [
'entity:filter_name' => 'foo',
'entity:filter_value' => 'bar',
'entity:filter2_name' => 'tree',
'entity:filter2_value' => 'house',
];
// Make sure we get the same value back.
user_filter_manager::set(5, $values);
$this->assertEqualsCanonicalizing($values, user_filter_manager::get(5));
user_filter_manager::merge(5, [
'entity:filter_name' => 'twotimesfoo',
'entity:filter_value' => 'twotimesbar',
]);
// Make sure that both values have been changed and the other values have not been modified.
$expected = [
'entity:filter_name' => 'twotimesfoo',
'entity:filter_value' => 'twotimesbar',
'entity:filter2_name' => 'tree',
'entity:filter2_value' => 'house',
];
$this->assertEqualsCanonicalizing($expected, user_filter_manager::get(5));
}
/**
* Test to get all filters from a given user
*/
public function test_get_all_for_user(): void {
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
$filtervalues1 = [
'entity:filter_name' => 'foo',
'entity:filter_value' => 'bar',
'entity:other_name' => 'baz',
'entity:other_value' => 'bax',
];
user_filter_manager::set(5, $filtervalues1);
$filtervalues2 = [
'entity:filter_name' => 'blue',
'entity:filter_value' => 'red',
];
user_filter_manager::set(9, $filtervalues2);
$this->setAdminUser();
$values = user_filter_manager::get_all_for_user((int)$user->id);
$this->assertEqualsCanonicalizing([$filtervalues1, $filtervalues2], [reset($values), end($values)]);
// Check for a user with no filters.
$user2 = $this->getDataGenerator()->create_user();
$values = user_filter_manager::get_all_for_user((int)$user2->id);
$this->assertEmpty($values);
}
}
@@ -0,0 +1,117 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\local\helpers;
use advanced_testcase;
use core_reportbuilder\local\entities\user;
use core_reportbuilder\local\report\column;
use core_reportbuilder\local\report\filter;
/**
* Unit tests for user profile fields helper
*
* @package core_reportbuilder
* @covers \core_reportbuilder\local\helpers\user_profile_fields
* @copyright 2021 David Matamoros <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class user_profile_fields_test_testcase extends advanced_testcase {
/**
* Generate userprofilefields
*/
private function generate_userprofilefields(): user_profile_fields {
$this->getDataGenerator()->create_custom_profile_field([
'shortname' => 'upf1', 'name' => 'User profile field text 1', 'datatype' => 'text']);
$this->getDataGenerator()->create_custom_profile_field([
'shortname' => 'upf2', 'name' => 'User profile field text 2', 'datatype' => 'text']);
$userentity = new user();
$useralias = $userentity->get_table_alias('user');
// Create an instance of the userprofilefield helper.
return new user_profile_fields("$useralias.id", $userentity->get_entity_name());
}
/**
* Test for get_columns
*/
public function test_get_columns(): void {
$this->resetAfterTest();
$userprofilefields = $this->generate_userprofilefields();
$columns = $userprofilefields->get_columns();
$this->assertCount(2, $columns);
[$column0, $column1] = $columns;
$this->assertInstanceOf(column::class, $column0);
$this->assertInstanceOf(column::class, $column1);
$this->assertEqualsCanonicalizing(['User profile field text 1', 'User profile field text 2'],
[$column0->get_title(), $column1->get_title()]);
$this->assertEquals(column::TYPE_TEXT, $column0->get_type());
$this->assertEquals('user', $column0->get_entity_name());
$this->assertStringStartsWith('LEFT JOIN {user_info_data}', $column0->get_joins()[0]);
}
/**
* Test for add_join
*/
public function test_add_join(): void {
$this->resetAfterTest();
$userprofilefields = $this->generate_userprofilefields();
$columns = $userprofilefields->get_columns();
$this->assertCount(1, ($columns[0])->get_joins());
$userprofilefields->add_join('JOIN {test} t ON t.id = id');
$columns = $userprofilefields->get_columns();
$this->assertCount(2, ($columns[0])->get_joins());
}
/**
* Test for add_joins
*/
public function test_add_joins(): void {
$this->resetAfterTest();
$userprofilefields = $this->generate_userprofilefields();
$columns = $userprofilefields->get_columns();
$this->assertCount(1, ($columns[0])->get_joins());
$userprofilefields->add_joins(['JOIN {test} t ON t.id = id', 'JOIN {test2} t2 ON t2.id = id']);
$columns = $userprofilefields->get_columns();
$this->assertCount(3, ($columns[0])->get_joins());
}
/**
* Test for get_filters
*/
public function test_get_filters(): void {
$this->resetAfterTest();
$userprofilefields = $this->generate_userprofilefields();
$filters = $userprofilefields->get_filters();
$this->assertCount(2, $filters);
[$filter0, $filter1] = $filters;
$this->assertInstanceOf(filter::class, $filter0);
$this->assertInstanceOf(filter::class, $filter1);
$this->assertEqualsCanonicalizing(['User profile field text 1', 'User profile field text 2'],
[$filter0->get_header(), $filter1->get_header()]);
}
}
@@ -0,0 +1,103 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\local\report;
use advanced_testcase;
use moodle_url;
use pix_icon;
use stdClass;
/**
* Unit tests for a report action
*
* @package core_reportbuilder
* @covers \core_reportbuilder\local\report\action
* @copyright 2021 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class action_testcase extends advanced_testcase {
/**
* Test adding a callback that returns true
*/
public function test_add_callback_true(): void {
$action = $this->create_action()
->add_callback(static function(stdClass $row): bool {
return true;
});
$this->assertNotNull($action->get_action_link(new stdClass()));
}
/**
* Test adding a callback that returns false
*/
public function test_add_callback_false(): void {
$action = $this->create_action()
->add_callback(static function(stdClass $row): bool {
return false;
});
$this->assertNull($action->get_action_link(new stdClass()));
}
/**
* Test that action link URL parameters have placeholders replaced
*/
public function test_get_action_link_url_parameters(): void {
$action = $this->create_action(['id' => ':id', 'action' => 'edit']);
$actionlink = $action->get_action_link((object) ['id' => 42]);
// This is the action URL we expect.
$expectedactionurl = (new moodle_url('/', ['id' => 42, 'action' => 'edit']))->out(false);
$this->assertStringContainsString("href=\"{$expectedactionurl}\"", $actionlink);
}
/**
* Test that action link attributes have placeholders replaced
*/
public function test_get_action_link_attributes(): void {
$action = $this->create_action([], ['data-id' => ':id', 'data-action' => 'edit']);
$actionlink = $action->get_action_link((object) ['id' => 42]);
// We expect each of these attributes to exist.
$expectedattributes = [
'data-id' => 42,
'data-action' => 'edit',
];
foreach ($expectedattributes as $key => $value) {
$this->assertStringContainsString("{$key}=\"{$value}\"", $actionlink);
}
}
/**
* Helper method to create an action instance
*
* @param array $urlparams
* @param array $attributes
* @return action
*/
private function create_action(array $urlparams = [], array $attributes = []): action {
return new action(
new moodle_url('/', $urlparams),
new pix_icon('t/edit', get_string('edit')),
$attributes
);
}
}
@@ -0,0 +1,153 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\local\report;
use advanced_testcase;
use context_system;
use core_reportbuilder\system_report_available;
use core_reportbuilder\system_report_factory;
/**
* Unit tests for report base class
*
* @package core_reportbuilder
* @covers \core_reportbuilder\local\report\base
* @copyright 2021 David Matamoros <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class base_testcase extends advanced_testcase {
/**
* Load required class
*/
public static function setUpBeforeClass(): void {
global $CFG;
require_once("{$CFG->dirroot}/reportbuilder/tests/fixtures/system_report_available.php");
}
/**
* Test for add_base_condition_simple
*/
public function test_add_base_condition_simple(): void {
$this->resetAfterTest();
$systemreport = system_report_factory::create(system_report_available::class, context_system::instance());
$systemreport->add_base_condition_simple('username', 'admin');
[$where, $params] = $systemreport->get_base_condition();
$this->assertStringMatchesFormat('username = :%a', $where);
$this->assertEqualsCanonicalizing(['admin'], $params);
}
/**
* Test for add_base_condition_simple null
*/
public function test_add_base_condition_simple_null(): void {
$this->resetAfterTest();
$systemreport = system_report_factory::create(system_report_available::class, context_system::instance());
$systemreport->add_base_condition_simple('username', null);
[$where, $params] = $systemreport->get_base_condition();
$this->assertEquals('username IS NULL', $where);
$this->assertEmpty($params);
}
/**
* Test for get_filter_instances
*/
public function test_get_filter_instances(): void {
$this->resetAfterTest();
$systemreport = system_report_factory::create(system_report_available::class, context_system::instance(),
'', '', 0, ['withfilters' => true]);
$filters = $systemreport->get_filter_instances();
$this->assertCount(1, $filters);
$this->assertInstanceOf(\core_reportbuilder\local\filters\text::class, reset($filters));
}
/**
* Test for set_downloadable
*/
public function test_set_downloadable(): void {
$this->resetAfterTest();
$systemreport = system_report_factory::create(system_report_available::class, context_system::instance());
$systemreport->set_downloadable(true, 'testfilename');
$this->assertTrue($systemreport->is_downloadable());
$this->assertEquals('testfilename', $systemreport->get_downloadfilename());
$systemreport->set_downloadable(false, 'anothertestfilename');
$this->assertFalse($systemreport->is_downloadable());
$this->assertEquals('anothertestfilename', $systemreport->get_downloadfilename());
}
/**
* Test for get_context
*/
public function test_get_context(): void {
$this->resetAfterTest();
$systemreport = system_report_factory::create(system_report_available::class, context_system::instance());
$this->assertEquals(context_system::instance(), $systemreport->get_context());
$course = $this->getDataGenerator()->create_course();
$contextcourse = \context_course::instance($course->id);
$systemreport2 = system_report_factory::create(system_report_available::class, $contextcourse);
$this->assertEquals($contextcourse, $systemreport2->get_context());
}
/**
* Test for get_column
*/
public function test_get_column(): void {
$this->resetAfterTest();
$systemreport = system_report_factory::create(system_report_available::class, context_system::instance());
$column = $systemreport->get_column('user:username');
$this->assertInstanceOf(column::class, $column);
$column = $systemreport->get_column('user:nonexistingcolumn');
$this->assertNull($column);
}
/**
* Test for get_filter
*/
public function test_get_filter(): void {
$this->resetAfterTest();
$systemreport = system_report_factory::create(system_report_available::class, context_system::instance(),
'', '', 0, ['withfilters' => true]);
$filter = $systemreport->get_filter('user:username');
$this->assertInstanceOf(filter::class, $filter);
$filter = $systemreport->get_filter('user:nonexistingfilter');
$this->assertNull($filter);
}
/**
* Test for get_report_persistent
*/
public function test_get_report_persistent(): void {
$this->resetAfterTest();
$systemreport = system_report_factory::create(system_report_available::class, context_system::instance());
$persistent = $systemreport->get_report_persistent();
$this->assertEquals(system_report_available::class, $persistent->get('source'));
}
}
@@ -0,0 +1,423 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\local\report;
use advanced_testcase;
use coding_exception;
use lang_string;
use stdClass;
use core_reportbuilder\local\helpers\database;
/**
* Unit tests for a report column
*
* @package core_reportbuilder
* @covers \core_reportbuilder\local\report\column
* @copyright 2020 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class column_testcase extends advanced_testcase {
/**
* Test column name getter/setter
*/
public function test_name(): void {
$column = $this->create_column('test');
$this->assertEquals('test', $column->get_name());
$this->assertEquals('another', $column
->set_name('another')
->get_name()
);
}
/**
* Test column title getter/setter
*/
public function test_title(): void {
$column = $this->create_column('test', new lang_string('show'));
$this->assertEquals('Show', $column->get_title());
$this->assertEquals('Hide', $column
->set_title(new lang_string('hide'))
->get_title()
);
// Column titles can also be empty.
$this->assertEmpty($column
->set_title(null)
->get_title());
}
/**
* Test entity name getter
*/
public function test_get_entity_name(): void {
$column = $this->create_column('test', null, 'entityname');
$this->assertEquals('entityname', $column->get_entity_name());
}
/**
* Test getting unique identifier
*/
public function test_get_unique_identifier(): void {
$column = $this->create_column('test', null, 'entityname');
$this->assertEquals('entityname:test', $column->get_unique_identifier());
}
/**
* Test column type getter/setter
*/
public function test_type(): void {
$column = $this->create_column('test');
$this->assertEquals(column::TYPE_TEXT, $column
->set_type(column::TYPE_TEXT)
->get_type());
}
/**
* Test column type with invalid value
*/
public function test_type_invalid(): void {
$column = $this->create_column('test');
$this->expectException(coding_exception::class);
$this->expectExceptionMessage('Invalid column type');
$column->set_type(-1);
}
/**
* Test adding single join
*/
public function test_add_join(): void {
$column = $this->create_column('test');
$this->assertEquals([], $column->get_joins());
$column->add_join('JOIN {user} u ON u.id = table.userid');
$this->assertEquals(['JOIN {user} u ON u.id = table.userid'], $column->get_joins());
}
/**
* Test adding multiple joins
*/
public function test_add_joins(): void {
$tablejoins = [
"JOIN {course} c2 ON c2.id = c1.id",
"JOIN {course} c3 ON c3.id = c1.id",
];
$column = $this->create_column('test')
->add_joins($tablejoins);
$this->assertEquals($tablejoins, $column->get_joins());
}
/**
* Data provider for {@see test_add_field}
*
* @return array
*/
public function add_field_provider(): array {
return [
['foo', '', ['foo AS c1_foo']],
['foo', 'bar', ['foo AS c1_bar']],
['t.foo', '', ['t.foo AS c1_foo']],
['t.foo', 'bar', ['t.foo AS c1_bar']],
];
}
/**
* Test adding single field, and retrieving it
*
* @param string $sql
* @param string $alias
* @param array $expectedselect
*
* @dataProvider add_field_provider
*/
public function test_add_field(string $sql, string $alias, array $expectedselect): void {
$column = $this->create_column('test')
->set_index(1)
->add_field($sql, $alias);
$this->assertEquals($expectedselect, $column->get_fields());
}
/**
* Test adding params to field, and retrieving them
*/
public function test_add_field_with_params(): void {
$param = database::generate_param_name();
$column = $this->create_column('test')
->set_index(1)
->add_field(":{$param}", 'foo', [$param => 'bar']);
// Select will look like the following: "p<index>_rbparam<counter>", where index is the column index and counter is
// a static value of the report helper class.
$select = $column->get_fields();
preg_match('/:(?<paramname>p1_rbparam[\d]+) AS c1_foo/', $select[0], $matches);
$this->assertArrayHasKey('paramname', $matches);
$this->assertEquals([$matches['paramname'] => 'bar'], $column->get_params());
}
/**
* Test adding field with alias as part of SQL throws an exception
*/
public function test_add_field_alias_in_sql(): void {
$column = $this->create_column('test')
->set_index(1);
$this->expectException(coding_exception::class);
$this->expectExceptionMessage('Column alias must be passed as a separate argument');
$column->add_field('foo AS bar');
}
/**
* Test adding field with complex SQL without an alias throws an exception
*/
public function test_add_field_complex_without_alias(): void {
global $DB;
$column = $this->create_column('test')
->set_index(1);
$this->expectException(coding_exception::class);
$this->expectExceptionMessage('Complex columns must have an alias');
$column->add_field($DB->sql_concat('foo', 'bar'));
}
/**
* Data provider for {@see test_add_fields}
*
* @return array
*/
public function add_fields_provider(): array {
return [
['t.foo', ['t.foo AS c1_foo']],
['t.foo bar', ['t.foo AS c1_bar']],
['t.foo AS bar', ['t.foo AS c1_bar']],
['t.foo1, t.foo2 bar, t.foo3 AS baz', ['t.foo1 AS c1_foo1', 't.foo2 AS c1_bar', 't.foo3 AS c1_baz']],
];
}
/**
* Test adding fields to a column, and retrieving them
*
* @param string $sql
* @param array $expectedselect
*
* @dataProvider add_fields_provider
*/
public function test_add_fields(string $sql, array $expectedselect): void {
$column = $this->create_column('test')
->set_index(1)
->add_fields($sql);
$this->assertEquals($expectedselect, $column->get_fields());
}
/**
* Test column alias
*/
public function test_column_alias(): void {
$column = $this->create_column('test')
->set_index(1)
->add_fields('t.foo, t.bar');
$this->assertEquals('c1_foo', $column->get_column_alias());
}
/**
* Test alias of column without any fields throws exception
*/
public function test_column_alias_no_fields(): void {
$column = $this->create_column('test');
$this->expectException(coding_exception::class);
$this->expectExceptionMessage('Column ' . $column->get_unique_identifier() . ' contains no fields');
$column->add_field($column->get_column_alias());
}
/**
* Data provider for {@see test_format_value}
*
* @return array[]
*/
public function format_value_provider(): array {
return [
[column::TYPE_INTEGER, 42],
[column::TYPE_TEXT, 'Hello'],
[column::TYPE_TIMESTAMP, HOURSECS],
[column::TYPE_BOOLEAN, 1, true],
[column::TYPE_FLOAT, 1.23],
[column::TYPE_LONGTEXT, 'Amigos'],
];
}
/**
* Test that column value is returned as correctly (value plus type)
*
* @param int $columntype
* @param mixed $value
* @param mixed|null $expected Expected value, or null to indicate it should be identical to value
*
* @dataProvider format_value_provider
*/
public function test_format_value(int $columntype, $value, $expected = null): void {
$column = $this->create_column('test')
->set_index(1)
->set_type($columntype)
->add_field('t.foo');
$this->assertSame($expected ?? $value, $column->format_value([
'c1_foo' => $value,
]));
}
/**
* Test that column value with callback is returned
*/
public function test_format_value_callback(): void {
$column = $this->create_column('test')
->set_index(1)
->add_field('t.foo')
->set_type(column::TYPE_INTEGER)
->add_callback(static function(int $value, stdClass $values) {
return $value * 2;
});
$this->assertEquals(84, $column->format_value([
'c1_bar' => 10,
'c1_foo' => 42,
]));
}
/**
* Test that column value with callback (using all fields) is returned
*/
public function test_format_value_callback_fields(): void {
$column = $this->create_column('test')
->set_index(1)
->add_fields('t.foo, t.baz')
->set_type(column::TYPE_INTEGER)
->add_callback(static function(int $value, stdClass $values) {
return $values->foo + $values->baz;
});
$this->assertEquals(60, $column->format_value([
'c1_bar' => 10,
'c1_foo' => 42,
'c1_baz' => 18,
]));
}
/**
* Test that column value with callback (using arguments) is returned
*/
public function test_format_value_callback_arguments(): void {
$column = $this->create_column('test')
->set_index(1)
->add_field('t.foo')
->set_type(column::TYPE_INTEGER)
->add_callback(static function(int $value, stdClass $values, int $argument) {
return $value - $argument;
}, 10);
$this->assertEquals(32, $column->format_value([
'c1_bar' => 10,
'c1_foo' => 42,
]));
}
/**
* Test adding multiple callbacks to a column
*/
public function test_add_multiple_callback(): void {
$column = $this->create_column('test')
->set_index(1)
->add_field('t.foo')
->set_type(column::TYPE_TEXT)
->add_callback(static function(string $value): string {
return strrev($value);
})
->add_callback(static function(string $value): string {
return strtoupper($value);
});
$this->assertEquals('LIONEL', $column->format_value([
'c1_foo' => 'lenoil',
]));
}
/**
* Test that setting column callback overwrites previous callbacks
*/
public function test_set_callback(): void {
$column = $this->create_column('test')
->set_index(1)
->add_field('t.foo')
->set_type(column::TYPE_TEXT)
->add_callback(static function(string $value): string {
return strrev($value);
})
->set_callback(static function(string $value): string {
return strtoupper($value);
});
$this->assertEquals('LENOIL', $column->format_value([
'c1_foo' => 'lenoil',
]));
}
/**
* Test is sortable
*/
public function test_is_sortable(): void {
$column = $this->create_column('test');
$this->assertFalse($column->get_is_sortable());
$column->set_is_sortable(true);
$this->assertTrue($column->get_is_sortable());
}
/**
* Test is available
*/
public function test_is_available(): void {
$column = $this->create_column('test');
$this->assertTrue($column->get_is_available());
$column->set_is_available(true);
$this->assertTrue($column->get_is_available());
}
/**
* Helper method to create a column instance
*
* @param string $name
* @param lang_string|null $title
* @param string $entityname
* @return column
*/
private function create_column(string $name, ?lang_string $title = null, string $entityname = 'column_testcase'): column {
return new column($name, $title, $entityname);
}
}
@@ -0,0 +1,219 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\local\report;
use advanced_testcase;
use lang_string;
use moodle_exception;
use core_reportbuilder\local\filters\text;
/**
* Unit tests for a report filter
*
* @package core_reportbuilder
* @covers \core_reportbuilder\local\report\filter
* @copyright 2021 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class filter_testcase extends advanced_testcase {
/**
* Test getting filter class
*/
public function test_get_filter_class(): void {
$filter = $this->create_filter('username');
$this->assertEquals(text::class, $filter->get_filter_class());
}
/**
* Test specifying invalid filter class
*/
public function test_invalid_filter_class(): void {
$this->expectException(moodle_exception::class);
$this->expectExceptionMessage('Invalid filter (sillyclass)');
new filter('sillyclass', 'username', new lang_string('username'), 'filter_testcase');
}
/**
* Test getting name
*/
public function test_get_name(): void {
$filter = $this->create_filter('username');
$this->assertEquals('username', $filter->get_name());
}
/**
* Test getting header
*/
public function test_get_header(): void {
$filter = $this->create_filter('username');
$this->assertEquals('Username', $filter->get_header());
}
/**
* Test setting header
*/
public function test_set_header(): void {
$filter = $this->create_filter('username')
->set_header(new lang_string('firstname'));
$this->assertEquals('First name', $filter->get_header());
}
/**
* Test getting entity name
*/
public function test_get_entity_name(): void {
$filter = $this->create_filter('username');
$this->assertEquals('filter_testcase', $filter->get_entity_name());
}
/**
* Test getting unique identifier
*/
public function test_get_unique_identifier(): void {
$filter = $this->create_filter('username');
$this->assertEquals('filter_testcase:username', $filter->get_unique_identifier());
}
/**
* Test getting field SQL
*/
public function test_get_field_sql(): void {
$filter = $this->create_filter('username', 'u.username');
$this->assertEquals('u.username', $filter->get_field_sql());
}
/**
* Test getting field params
*/
public function test_get_field_params(): void {
$filter = $this->create_filter('username', 'u.username = :foo', ['foo' => 'bar']);
$this->assertEquals(['foo' => 'bar'], $filter->get_field_params());
}
/**
* Test adding single join
*/
public function test_add_join(): void {
$filter = $this->create_filter('username', 'u.username');
$this->assertEquals([], $filter->get_joins());
$filter->add_join('JOIN {user} u ON u.id = table.userid');
$this->assertEquals(['JOIN {user} u ON u.id = table.userid'], $filter->get_joins());
}
/**
* Test adding multiple joins
*/
public function test_add_joins(): void {
$tablejoins = [
"JOIN {course} c2 ON c2.id = c1.id",
"JOIN {course} c3 ON c3.id = c1.id",
];
$filter = $this->create_filter('username', 'u.username')
->add_joins($tablejoins);
$this->assertEquals($tablejoins, $filter->get_joins());
}
/**
* Test is available
*/
public function test_is_available(): void {
$filter = $this->create_filter('username', 'u.username');
$this->assertTrue($filter->get_is_available());
$filter->set_is_available(true);
$this->assertTrue($filter->get_is_available());
}
/**
* Test setting filter options
*/
public function test_set_options(): void {
$filter = $this->create_filter('username', 'u.username')
->set_options([1, 2, 3]);
$this->assertEquals([1, 2, 3], $filter->get_options());
}
/**
* Test setting filter options via callback
*/
public function test_set_options_callback(): void {
$filter = $this->create_filter('username', 'u.username')
->set_options_callback(static function() {
return 10 * 5;
});
$this->assertEquals(50, $filter->get_options());
}
/**
* Test restricting filter operators
*/
public function test_limited_operators(): void {
$filter = $this->create_filter('username', 'u.username')
->set_limited_operators([
text::IS_EQUAL_TO,
text::IS_NOT_EQUAL_TO,
]);
$limitedoperators = $filter->restrict_limited_operators([
text::CONTAINS => 'Contains',
text::DOES_NOT_CONTAIN => 'Does not contain',
text::IS_EQUAL_TO => 'Is equal to',
text::IS_NOT_EQUAL_TO => 'Is not equal to',
]);
$this->assertEquals([
text::IS_EQUAL_TO => 'Is equal to',
text::IS_NOT_EQUAL_TO => 'Is not equal to',
], $limitedoperators);
}
/**
* Test not restricting filter operators
*/
public function test_unlimited_operators(): void {
$filter = $this->create_filter('username', 'u.username');
$operators = [
text::CONTAINS => 'Contains',
text::DOES_NOT_CONTAIN => 'Does not contain',
];
// If no operator limit has been set for the filter, then all available operators should be present.
$this->assertEquals($operators, $filter->restrict_limited_operators($operators));
}
/**
* Helper method to create a filter instance
*
* @param string $name
* @param string $fieldsql
* @param array $fieldparams
* @return filter
*/
private function create_filter(string $name, string $fieldsql = '', array $fieldparams = []): filter {
return new filter(text::class, $name, new lang_string($name), 'filter_testcase', $fieldsql, $fieldparams);
}
}
+132
View File
@@ -0,0 +1,132 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder;
use advanced_testcase;
use context_system;
use stdClass;
use core_reportbuilder\local\models\report;
use core_reportbuilder\local\report\base;
/**
* Unit tests for the report manager class
*
* @package core_reportbuilder
* @covers \core_reportbuilder\manager
* @copyright 2020 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class manager_testcase extends advanced_testcase {
/**
* Test creating a report instance from persistent
*/
public function test_get_report_from_persistent(): void {
global $CFG;
require_once("{$CFG->dirroot}/reportbuilder/tests/fixtures/system_report_available.php");
$this->resetAfterTest();
$report = manager::create_report_persistent((object) [
'type' => base::TYPE_SYSTEM_REPORT,
'source' => system_report_available::class,
]);
$systemreport = manager::get_report_from_persistent($report);
$this->assertInstanceOf(system_report::class, $systemreport);
}
/**
* Test creating a report instance from persistent with an invalid source
*/
public function test_get_report_from_persistent_invalid(): void {
$this->resetAfterTest();
$report = manager::create_report_persistent((object) [
'type' => base::TYPE_SYSTEM_REPORT,
'source' => stdClass::class,
]);
$this->expectException(source_invalid_exception::class);
manager::get_report_from_persistent($report);
}
/**
* Test creating a report instance from persistent with an unavailable source
*/
public function test_get_report_from_persistent_unavailable(): void {
global $CFG;
require_once("{$CFG->dirroot}/reportbuilder/tests/fixtures/system_report_unavailable.php");
$this->resetAfterTest();
$report = manager::create_report_persistent((object) [
'type' => base::TYPE_SYSTEM_REPORT,
'source' => system_report_unavailable::class,
]);
$this->expectException(source_unavailable_exception::class);
manager::get_report_from_persistent($report);
}
/**
* Test report source exists
*/
public function test_report_source_exists(): void {
global $CFG;
require_once("{$CFG->dirroot}/reportbuilder/tests/fixtures/system_report_available.php");
$this->assertTrue(manager::report_source_exists(system_report_available::class));
$this->assertFalse(manager::report_source_exists(stdClass::class));
}
/**
* Test report source available
*/
public function test_report_source_available(): void {
global $CFG;
require_once("{$CFG->dirroot}/reportbuilder/tests/fixtures/system_report_available.php");
$this->assertTrue(manager::report_source_available(system_report_available::class));
require_once("{$CFG->dirroot}/reportbuilder/tests/fixtures/system_report_unavailable.php");
$this->assertFalse(manager::report_source_available(system_report_unavailable::class));
}
/**
* Test creating a report persistent model
*/
public function test_create_report_persistent(): void {
global $CFG;
require_once("{$CFG->dirroot}/reportbuilder/tests/fixtures/system_report_available.php");
$this->resetAfterTest();
$report = manager::create_report_persistent((object) [
'type' => base::TYPE_SYSTEM_REPORT,
'source' => \core_reportbuilder\system_report_available::class,
]);
$this->assertInstanceOf(report::class, $report);
$this->assertEquals(base::TYPE_SYSTEM_REPORT, $report->get('type'));
$this->assertEquals(system_report_available::class, $report->get('source'));
$this->assertInstanceOf(context_system::class, $report->get_context());
}
}
@@ -0,0 +1,112 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\privacy;
use context_system;
use core_privacy\local\metadata\collection;
use core_privacy\local\metadata\types\database_table;
use core_privacy\local\metadata\types\user_preference;
use core_privacy\local\request\writer;
use core_reportbuilder\manager;
use core_reportbuilder\local\helpers\user_filter_manager;
/**
* Unit tests for privacy provider
*
* @package core_reportbuilder
* @covers \core_reportbuilder\privacy\provider
* @copyright 2021 David Matamoros <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider_testcase extends \core_privacy\tests\provider_testcase {
/**
* Test provider metadata
*/
public function test_get_metadata(): void {
$collection = new collection('core_reportbuilder');
$metadata = provider::get_metadata($collection)->get_collection();
$this->assertCount(2, $metadata);
$this->assertInstanceOf(database_table::class, $metadata[0]);
$this->assertEquals('reportbuilder_report', $metadata[0]->get_name());
$this->assertInstanceOf(user_preference::class, $metadata[1]);
}
/**
* Test to check export_user_preferences.
*/
public function test_export_user_preferences(): void {
$this->resetAfterTest();
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
$this->setUser($user1);
// Create report and set some filters for the user.
$report1 = manager::create_report_persistent((object) [
'type' => 1,
'source' => 'class',
]);
$filtervalues1 = [
'task_log:name_operator' => 0,
'task_log:name_value' => 'My task logs',
];
user_filter_manager::set($report1->get('id'), $filtervalues1);
// Add a filter for user2.
$filtervalues1user2 = [
'task_log:name_operator' => 0,
'task_log:name_value' => 'My task logs user2',
];
user_filter_manager::set($report1->get('id'), $filtervalues1user2, (int)$user2->id);
// Create a second report and set some filters for the user.
$report2 = manager::create_report_persistent((object) [
'type' => 1,
'source' => 'class',
]);
$filtervalues2 = [
'config_change:setting_operator' => 0,
'config_change:setting_value' => str_repeat('A', 3000),
];
user_filter_manager::set($report2->get('id'), $filtervalues2);
// Switch to admin user (so we can validate preferences of our test user are still exported).
$this->setAdminUser();
// Export user preferences.
provider::export_user_preferences((int)$user1->id);
$writer = writer::with_context(context_system::instance());
$prefs = $writer->get_user_preferences('core_reportbuilder');
// Check that user preferences only contain the 2 preferences from user1.
$this->assertCount(2, (array)$prefs);
// Check that exported user preferences for report1 are correct.
$report1key = 'reportbuilder-report-' . $report1->get('id');
$this->assertEquals(json_encode($filtervalues1, JSON_PRETTY_PRINT), $prefs->$report1key->value);
// Check that exported user preferences for report2 are correct.
$report2key = 'reportbuilder-report-' . $report2->get('id');
$this->assertEquals(json_encode($filtervalues2, JSON_PRETTY_PRINT), $prefs->$report2key->value);
}
}
@@ -0,0 +1,85 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder;
use advanced_testcase;
use context_system;
use stdClass;
/**
* Unit tests for the system report factory class
*
* @package core_reportbuilder
* @covers \core_reportbuilder\system_report_factory
* @copyright 2020 Paul Holden <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class system_report_factory_testcase extends advanced_testcase {
/**
* Test creating a valid/available system report
*/
public function test_create(): void {
global $CFG;
require_once("{$CFG->dirroot}/reportbuilder/tests/fixtures/system_report_available.php");
$this->resetAfterTest();
$systemreport = system_report_factory::create(system_report_available::class, context_system::instance());
$this->assertInstanceOf(system_report::class, $systemreport);
}
/**
* Test that creating a system report returns the same persistent if it already exists
*/
public function test_create_previously_exists(): void {
global $CFG;
require_once("{$CFG->dirroot}/reportbuilder/tests/fixtures/system_report_available.php");
$this->resetAfterTest();
$systemreportone = system_report_factory::create(system_report_available::class, context_system::instance());
$systemreporttwo = system_report_factory::create(system_report_available::class, context_system::instance());
// Assert we have the same persistent for each.
$this->assertEquals($systemreportone->get_report_persistent()->get('id'),
$systemreporttwo->get_report_persistent()->get('id'));
}
/**
* Test creating an system report with an invalid source
*/
public function test_create_invalid(): void {
$this->expectException(source_invalid_exception::class);
system_report_factory::create(stdClass::class, context_system::instance());
}
/**
* Test creating an unavailable system report
*/
public function test_create_unavailable(): void {
global $CFG;
require_once("{$CFG->dirroot}/reportbuilder/tests/fixtures/system_report_unavailable.php");
$this->resetAfterTest();
$this->expectException(source_unavailable_exception::class);
system_report_factory::create(system_report_unavailable::class, context_system::instance());
}
}
@@ -0,0 +1,94 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder;
use advanced_testcase;
use context_system;
use core_reportbuilder\local\report\action;
/**
* Unit tests for the system report class
*
* @package core_reportbuilder
* @covers \core_reportbuilder\system_report
* @copyright 2021 David Matamoros <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class system_report_testcase extends advanced_testcase {
/**
* Test for actions
*/
public function test_actions(): void {
global $CFG;
require_once("{$CFG->dirroot}/reportbuilder/tests/fixtures/system_report_available.php");
$this->resetAfterTest();
$systemreport = system_report_factory::create(system_report_available::class, context_system::instance(),
'', '', 0, ['withactions' => true]);
$actions = $systemreport->get_actions();
$this->assertCount(1, $actions);
$action = reset($actions);
$this->assertInstanceOf(action::class, $action);
$systemreport->add_action($action);
$actions = $systemreport->get_actions();
$this->assertCount(2, $actions);
$this->assertTrue($systemreport->has_actions());
}
/**
* Test for parameters
*/
public function test_parameters(): void {
global $CFG;
require_once("{$CFG->dirroot}/reportbuilder/tests/fixtures/system_report_available.php");
$this->resetAfterTest();
$systemreport = system_report_factory::create(system_report_available::class, context_system::instance(),
'', '', 0, ['withactions' => true]);
$this->assertEquals(['withactions' => true], $systemreport->get_parameters());
$systemreport->set_parameters(['withfilters' => true, 'secondparameter' => false]);
$this->assertEquals(['withfilters' => true, 'secondparameter' => false], $systemreport->get_parameters());
$this->assertFalse((bool)$systemreport->get_parameter('secondparameter', true, PARAM_BOOL));
// Get a param that does not exist.
$this->assertEquals('3', $systemreport->get_parameter('thirdparameter', '3', PARAM_INT));
}
/**
* Test for column sorting
*/
public function test_initial_sort_column(): void {
global $CFG;
require_once("{$CFG->dirroot}/reportbuilder/tests/fixtures/system_report_available.php");
$this->resetAfterTest();
$systemreport = system_report_factory::create(system_report_available::class, context_system::instance());
$systemreport->set_initial_sort_column('user:username', SORT_DESC);
$column = $systemreport->get_column('user:username');
$this->assertEquals($column, $systemreport->get_initial_sort_column());
$this->assertEquals(SORT_DESC, $systemreport->get_initial_sort_direction());
}
}
+1
View File
@@ -43,3 +43,4 @@ $breadcrumb-divider-rtl: "◀" !default;
@import "moodle/atto";
@import "moodle/toasts";
@import "moodle/navbar";
@import "moodle/reportbuilder";
@@ -0,0 +1,46 @@
/* Rportbuilder */
.reportbuilder-table-wrapper {
.filters-dropdown {
width: 27rem;
padding: 0;
@include media-breakpoint-down(sm) {
width: 100%;
}
}
.reportbuilder-filters-wrapper {
.mform {
&.full-width-labels {
.fitem.row {
> .col-md-3,
> .col-md-9 {
flex: 0 0 100%;
max-width: 100%;
}
.fdate_selector {
flex-wrap: wrap;
}
}
}
.form-group {
margin-bottom: 0;
}
}
.filter {
.filter-header {
font-size: $h5-font-size;
.filter-name {
font-size: 1rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding-right: 1rem;
}
.filter-name:hover {
white-space: normal;
text-overflow: clip;
word-break: break-all;
}
}
}
}
}
+32
View File
@@ -20017,6 +20017,38 @@ div.editor_atto_toolbar button .icon {
#page {
margin-top: 50px; }
/* Rportbuilder */
.reportbuilder-table-wrapper .filters-dropdown {
width: 27rem;
padding: 0; }
@media (max-width: 767.98px) {
.reportbuilder-table-wrapper .filters-dropdown {
width: 100%; } }
.reportbuilder-table-wrapper .reportbuilder-filters-wrapper .mform.full-width-labels .fitem.row > .col-md-3,
.reportbuilder-table-wrapper .reportbuilder-filters-wrapper .mform.full-width-labels .fitem.row > .col-md-9 {
flex: 0 0 100%;
max-width: 100%; }
.reportbuilder-table-wrapper .reportbuilder-filters-wrapper .mform.full-width-labels .fitem.row .fdate_selector {
flex-wrap: wrap; }
.reportbuilder-table-wrapper .reportbuilder-filters-wrapper .mform .form-group {
margin-bottom: 0; }
.reportbuilder-table-wrapper .reportbuilder-filters-wrapper .filter .filter-header {
font-size: 1.171875rem; }
.reportbuilder-table-wrapper .reportbuilder-filters-wrapper .filter .filter-header .filter-name {
font-size: 1rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding-right: 1rem; }
.reportbuilder-table-wrapper .reportbuilder-filters-wrapper .filter .filter-header .filter-name:hover {
white-space: normal;
text-overflow: clip;
word-break: break-all; }
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; }
+32
View File
@@ -20208,6 +20208,38 @@ div.editor_atto_toolbar button .icon {
#page {
margin-top: 50px; }
/* Rportbuilder */
.reportbuilder-table-wrapper .filters-dropdown {
width: 27rem;
padding: 0; }
@media (max-width: 767.98px) {
.reportbuilder-table-wrapper .filters-dropdown {
width: 100%; } }
.reportbuilder-table-wrapper .reportbuilder-filters-wrapper .mform.full-width-labels .fitem.row > .col-md-3,
.reportbuilder-table-wrapper .reportbuilder-filters-wrapper .mform.full-width-labels .fitem.row > .col-md-9 {
flex: 0 0 100%;
max-width: 100%; }
.reportbuilder-table-wrapper .reportbuilder-filters-wrapper .mform.full-width-labels .fitem.row .fdate_selector {
flex-wrap: wrap; }
.reportbuilder-table-wrapper .reportbuilder-filters-wrapper .mform .form-group {
margin-bottom: 0; }
.reportbuilder-table-wrapper .reportbuilder-filters-wrapper .filter .filter-header {
font-size: 1.171875rem; }
.reportbuilder-table-wrapper .reportbuilder-filters-wrapper .filter .filter-header .filter-name {
font-size: 1rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding-right: 1rem; }
.reportbuilder-table-wrapper .reportbuilder-filters-wrapper .filter .filter-header .filter-name:hover {
white-space: normal;
text-overflow: clip;
word-break: break-all; }
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; }
+1 -1
View File
@@ -29,7 +29,7 @@
defined('MOODLE_INTERNAL') || die();
$version = 2021072800.00; // YYYYMMDD = weekly release date of this DEV branch.
$version = 2021072800.01; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
$release = '4.0dev (Build: 20210728)'; // Human-friendly version name