diff --git a/lib/db/services.php b/lib/db/services.php index 7da191981ae..c3ebbffc178 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -2818,6 +2818,12 @@ $functions = array( 'type' => 'read', 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], ], + 'core_reportbuilder_retrieve_report' => [ + 'classname' => 'core_reportbuilder\external\reports\retrieve', + 'description' => 'Retrieve custom report content', + 'type' => 'read', + 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], + ], 'core_reportbuilder_view_report' => [ 'classname' => 'core_reportbuilder\external\reports\view', 'description' => 'Trigger custom report viewed', diff --git a/reportbuilder/classes/external/custom_report_data_exporter.php b/reportbuilder/classes/external/custom_report_data_exporter.php new file mode 100644 index 00000000000..3faf5777e2c --- /dev/null +++ b/reportbuilder/classes/external/custom_report_data_exporter.php @@ -0,0 +1,107 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\external; + +use renderer_base; +use core\external\exporter; +use core_reportbuilder\datasource; +use core_reportbuilder\table\custom_report_table_view; + +/** + * Custom report data exporter class + * + * @package core_reportbuilder + * @copyright 2022 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class custom_report_data_exporter extends exporter { + + /** + * Return a list of objects that are related to the exporter + * + * @return array + */ + protected static function define_related(): array { + return [ + 'report' => datasource::class, + 'page' => 'int', + 'perpage' => 'int', + ]; + } + + /** + * Return the list of additional properties for read structure and export + * + * @return array[] + */ + protected static function define_other_properties(): array { + return [ + 'headers' => [ + 'type' => PARAM_RAW, + 'multiple' => true, + ], + 'rows' => [ + 'type' => [ + 'columns' => [ + 'type' => PARAM_RAW, + 'null' => NULL_ALLOWED, + 'multiple' => true, + ], + ], + 'multiple' => true, + ], + 'totalrowcount' => ['type' => PARAM_INT], + ]; + } + + /** + * Get the additional values to inject while exporting + * + * @param renderer_base $output + * @return array + */ + protected function get_other_values(renderer_base $output): array { + global $DB; + + /** @var datasource $report */ + $report = $this->related['report']; + + $table = custom_report_table_view::create($report->get_report_persistent()->get('id')); + $table->setup(); + + // Internally the current page is zero-based, but this method expects value plus one. + $table->set_page_number($this->related['page'] + 1); + $table->query_db($this->related['perpage'], false); + + $tablerows = []; + foreach ($table->rawdata as $record) { + $tablerows[] = [ + 'columns' => array_values($table->format_row($record)), + ]; + } + + $table->close_recordset(); + + return [ + 'headers' => $table->headers, + 'rows' => $tablerows, + 'totalrowcount' => $DB->count_records_sql($table->countsql, $table->countparams), + ]; + } +} diff --git a/reportbuilder/classes/external/reports/retrieve.php b/reportbuilder/classes/external/reports/retrieve.php new file mode 100644 index 00000000000..a0209343811 --- /dev/null +++ b/reportbuilder/classes/external/reports/retrieve.php @@ -0,0 +1,107 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\external\reports; + +use core_reportbuilder\manager; +use external_api; +use external_function_parameters; +use external_single_structure; +use external_value; +use external_warnings; +use core_reportbuilder\permission; +use core_reportbuilder\external\{custom_report_data_exporter, custom_report_details_exporter}; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once("{$CFG->libdir}/externallib.php"); + +/** + * External method for retrieving custom report content + * + * @package core_reportbuilder + * @copyright 2022 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class retrieve 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'), + 'page' => new external_value(PARAM_INT, 'Page number', VALUE_DEFAULT, 0), + 'perpage' => new external_value(PARAM_INT, 'Reports per page', VALUE_DEFAULT, 10), + ]); + } + + /** + * External method execution + * + * @param int $reportid + * @param int $page + * @param int $perpage + * @return array + */ + public static function execute(int $reportid, int $page = 0, int $perpage = 10): array { + global $PAGE; + + [ + 'reportid' => $reportid, + 'page' => $page, + 'perpage' => $perpage, + ] = self::validate_parameters(self::execute_parameters(), [ + 'reportid' => $reportid, + 'page' => $page, + 'perpage' => $perpage, + ]); + + $report = manager::get_report_from_id($reportid); + self::validate_context($report->get_context()); + + $persistent = $report->get_report_persistent(); + permission::require_can_view_report($persistent); + + $output = $PAGE->get_renderer('core'); + + return [ + 'details' => (array) (new custom_report_details_exporter($persistent))->export($output), + 'data' => (array) (new custom_report_data_exporter(null, [ + 'report' => $report, 'page' => $page, 'perpage' => $perpage, + ]))->export($output), + 'warnings' => [], + ]; + } + + /** + * External method return value + * + * @return external_single_structure + */ + public static function execute_returns(): external_single_structure { + return new external_single_structure([ + 'details' => custom_report_details_exporter::get_read_structure(), + 'data' => custom_report_data_exporter::get_read_structure(), + 'warnings' => new external_warnings(), + ]); + } +} diff --git a/reportbuilder/tests/external/custom_report_data_exporter_test.php b/reportbuilder/tests/external/custom_report_data_exporter_test.php new file mode 100644 index 00000000000..4bd1302a3c0 --- /dev/null +++ b/reportbuilder/tests/external/custom_report_data_exporter_test.php @@ -0,0 +1,72 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\external; + +use advanced_testcase; +use core_reportbuilder_generator; +use core_reportbuilder\manager; +use core_user\reportbuilder\datasource\users; + +/** + * Unit tests for custom report data exporter + * + * @package core_reportbuilder + * @covers \core_reportbuilder\external\custom_report_data_exporter + * @copyright 2022 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class custom_report_data_exporter_test extends advanced_testcase { + + /** + * Test exported data structure + */ + public function test_export(): void { + global $PAGE; + + $this->resetAfterTest(); + + $this->getDataGenerator()->create_user(['firstname' => 'Zoe', 'lastname' => 'Zebra', 'email' => 'u1@example.com']); + $this->getDataGenerator()->create_user(['firstname' => 'Charlie', 'lastname' => 'Carrot', 'email' => 'u2@example.com']); + + /** @var core_reportbuilder_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); + + $report = $generator->create_report(['name' => 'My report', 'source' => users::class, 'default' => false]); + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:fullname']) + ->set_many(['heading' => 'Lovely user', 'sortenabled' => true])->update(); + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:email']); + + $reportinstance = manager::get_report_from_persistent($report); + + $exporter = new custom_report_data_exporter(null, ['report' => $reportinstance, 'page' => 0, 'perpage' => 2]); + $export = $exporter->export($PAGE->get_renderer('core_reportbuilder')); + + // There are three users (admin plus the two previouly created), but we're paging the first two only. + $this->assertEquals(['Lovely user', 'Email address'], $export->headers); + $this->assertEquals([ + [ + 'columns' => ['Admin User', 'admin@example.com'], + ], + [ + 'columns' => ['Charlie Carrot', 'u2@example.com'], + ], + ], $export->rows); + $this->assertEquals(3, $export->totalrowcount); + } +} diff --git a/reportbuilder/tests/external/reports/retrieve_test.php b/reportbuilder/tests/external/reports/retrieve_test.php new file mode 100644 index 00000000000..985390063fd --- /dev/null +++ b/reportbuilder/tests/external/reports/retrieve_test.php @@ -0,0 +1,116 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\external\reports; + +use core_reportbuilder_generator; +use external_api; +use externallib_advanced_testcase; +use core_reportbuilder\report_access_exception; +use core_user\reportbuilder\datasource\users; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once("{$CFG->dirroot}/webservice/tests/helpers.php"); + +/** + * Unit tests of external class for retrieving custom report content + * + * @package core_reportbuilder + * @covers \core_reportbuilder\external\reports\retrieve + * @copyright 2022 Paul Holden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class retrieve_test extends externallib_advanced_testcase { + + /** + * Text execute method + */ + public function test_execute(): void { + $this->resetAfterTest(); + $this->setAdminUser(); + + $this->getDataGenerator()->create_user(['firstname' => 'Zoe', 'lastname' => 'Zebra', 'email' => 'u1@example.com']); + $this->getDataGenerator()->create_user(['firstname' => 'Charlie', 'lastname' => 'Carrot', 'email' => 'u2@example.com']); + + /** @var core_reportbuilder_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); + + $report = $generator->create_report(['name' => 'My report', 'source' => users::class, 'default' => false]); + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:fullname']) + ->set('sortenabled', true)->update(); + $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:email']); + + // There are three users (admin plus the two previouly created), but we're paging the first two only. + $result = retrieve::execute($report->get('id'), 0, 2); + $result = external_api::clean_returnvalue(retrieve::execute_returns(), $result); + + // All data is generated by exporters, just assert relevant sample of each. + $this->assertArrayHasKey('details', $result); + $this->assertEquals('My report', $result['details']['name']); + + $this->assertArrayHasKey('data', $result); + $this->assertEquals(['Full name', 'Email address'], $result['data']['headers']); + $this->assertEquals([ + [ + 'columns' => ['Admin User', 'admin@example.com'], + ], + [ + 'columns' => ['Charlie Carrot', 'u2@example.com'], + ], + ], $result['data']['rows']); + $this->assertEquals(3, $result['data']['totalrowcount']); + $this->assertEmpty($result['warnings']); + + // Retrieve the second set of pages results. + $result = retrieve::execute($report->get('id'), 1, 2); + $result = external_api::clean_returnvalue(retrieve::execute_returns(), $result); + + $this->assertArrayHasKey('details', $result); + $this->assertEquals('My report', $result['details']['name']); + + $this->assertArrayHasKey('data', $result); + $this->assertEquals(['Full name', 'Email address'], $result['data']['headers']); + $this->assertEquals([ + [ + 'columns' => ['Zoe Zebra', 'u1@example.com'], + ], + ], $result['data']['rows']); + $this->assertEquals(3, $result['data']['totalrowcount']); + $this->assertEmpty($result['warnings']); + } + + /** + * Test execute method for a user without permission to view report + */ + public function test_execute_access_exception(): void { + $this->resetAfterTest(); + + /** @var core_reportbuilder_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); + $report = $generator->create_report(['name' => 'My report', 'source' => users::class]); + + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + + $this->expectException(report_access_exception::class); + $this->expectExceptionMessage('You cannot view this report'); + retrieve::execute($report->get('id')); + } +} diff --git a/reportbuilder/upgrade.txt b/reportbuilder/upgrade.txt index 8a34bc4cd40..9766e9bac65 100644 --- a/reportbuilder/upgrade.txt +++ b/reportbuilder/upgrade.txt @@ -11,6 +11,7 @@ Information provided here is intended especially for developers. some system reports * New external methods for retrieving custom report data: - `core_reportbuilder_list_reports` + - `core_reportbuilder_retrieve_report` - `core_reportbuilder_view_report` * 'set_default_per_page' and 'get_default_per_page' methods have been added to \local\report\base class to manage the default displayed rows per page. diff --git a/version.php b/version.php index 38b6beea2d2..479570f3707 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2022101100.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2022101100.01; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. $release = '4.1dev+ (Build: 20221011)'; // Human-friendly version name