This commit is contained in:
Andrew Nicols 2022-10-06 09:39:45 +08:00
commit 3589299d82
10 changed files with 482 additions and 62 deletions

View File

@ -2812,6 +2812,12 @@ $functions = array(
'type' => 'read',
'ajax' => true,
],
'core_reportbuilder_list_reports' => [
'classname' => 'core_reportbuilder\external\reports\listing',
'description' => 'List custom reports for current user',
'type' => 'read',
'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE],
],
'core_reportbuilder_view_report' => [
'classname' => 'core_reportbuilder\external\reports\view',
'description' => 'Trigger custom report viewed',

View File

@ -0,0 +1,80 @@
<?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_user;
use renderer_base;
use core\external\persistent_exporter;
use core_reportbuilder\datasource;
use core_reportbuilder\manager;
use core_reportbuilder\local\models\report;
use core_user\external\user_summary_exporter;
/**
* Custom report details exporter class
*
* @package core_reportbuilder
* @copyright 2022 Paul Holden <paulh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class custom_report_details_exporter extends persistent_exporter {
/** @var report The persistent object we will export. */
protected $persistent = null;
/**
* Return the name of the class we are exporting
*
* @return string
*/
protected static function define_class(): string {
return report::class;
}
/**
* Return a list of additional properties used only for display
*
* @return array
*/
protected static function define_other_properties(): array {
return [
'sourcename' => [
'type' => PARAM_RAW,
'null' => NULL_ALLOWED,
],
'modifiedby' => ['type' => user_summary_exporter::read_properties_definition()],
];
}
/**
* Get additional values to inject while exporting
*
* @param renderer_base $output
* @return array
*/
protected function get_other_values(renderer_base $output): array {
$source = $this->persistent->get('source');
$usermodified = core_user::get_user($this->persistent->get('usermodified'));
return [
'sourcename' => manager::report_source_exists($source, datasource::class) ? $source::get_name() : null,
'modifiedby' => (new user_summary_exporter($usermodified))->export($output),
];
}
}

View File

@ -0,0 +1,113 @@
<?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\reports;
use context_system;
use external_api;
use external_function_parameters;
use external_multiple_structure;
use external_single_structure;
use external_value;
use external_warnings;
use stdClass;
use core_reportbuilder\permission;
use core_reportbuilder\external\custom_report_details_exporter;
use core_reportbuilder\local\helpers\audience;
use core_reportbuilder\local\models\report;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once("{$CFG->libdir}/externallib.php");
/**
* External method for listing users' custom reports
*
* @package core_reportbuilder
* @copyright 2022 Paul Holden <paulh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class listing extends external_api {
/**
* External method parameters
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'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 $page
* @param int $perpage
* @return array
*/
public static function execute(int $page = 0, int $perpage = 10): array {
global $DB, $PAGE;
[
'page' => $page,
'perpage' => $perpage,
] = self::validate_parameters(self::execute_parameters(), [
'page' => $page,
'perpage' => $perpage,
]);
$context = context_system::instance();
self::validate_context($context);
permission::require_can_view_reports_list(null, $context);
// Filter list of reports by those the user can access.
[$where, $params] = audience::user_reports_list_access_sql('r');
$reports = $DB->get_records_sql("
SELECT r.*
FROM {" . report::TABLE . "} r
WHERE r.type = 0 AND {$where}
ORDER BY r.name, r.id", $params, $page * $perpage, $perpage);
$output = $PAGE->get_renderer('core');
return [
'reports' => array_map(static function(stdClass $report) use ($output): array {
$exporter = new custom_report_details_exporter(new report(0, $report));
return (array) $exporter->export($output);
}, $reports),
'warnings' => [],
];
}
/**
* External method return value
*
* @return external_single_structure
*/
public static function execute_returns(): external_single_structure {
return new external_single_structure([
'reports' => new external_multiple_structure(custom_report_details_exporter::get_read_structure()),
'warnings' => new external_warnings(),
]);
}
}

View File

@ -19,6 +19,8 @@ declare(strict_types=1);
namespace core_reportbuilder\local\helpers;
use cache;
use context;
use context_system;
use core_collator;
use core_component;
use core_plugin_manager;
@ -52,11 +54,11 @@ class audience {
}
/**
* Returns list of reports that the specified user can access. Note this is potentially very expensive to calculate if a
* Returns list of report IDs that the specified user can access, based on audience configuration. This can be expensive if the
* site has lots of reports, with lots of audiences, so we cache the result for the duration of the users session
*
* @param int|null $userid User ID to check, or the current user if omitted
* @return array
* @return int[]
*/
public static function get_allowed_reports(?int $userid = null): array {
global $USER, $DB;
@ -113,7 +115,7 @@ class audience {
}
/**
* Generate SQL select clause and params for selecting reports specified user can access
* Generate SQL select clause and params for selecting reports specified user can access, based on audience configuration
*
* @param string $reporttablealias
* @param int|null $userid User ID to check, or the current user if omitted
@ -137,7 +139,7 @@ class audience {
}
/**
* Return list of report ID's specified user can access
* Return list of report ID's specified user can access, based on audience configuration
*
* @param int|null $userid User ID to check, or the current user if omitted
* @return int[]
@ -153,6 +155,53 @@ class audience {
return $DB->get_fieldset_sql($sql, $params);
}
/**
* Returns SQL to limit the list of reports to those that the given user has access to
*
* - A user with 'editall' capability will have access to all reports
* - A user with 'edit' capability will have access to:
* - Those reports this user has created
* - Those reports this user is in audience of
* - A user with 'view' capability will have access to:
* - Those reports this user is in audience of
*
* @param string $reporttablealias
* @param int|null $userid User ID to check, or the current user if omitted
* @param context|null $context
* @return array
*/
public static function user_reports_list_access_sql(
string $reporttablealias,
?int $userid = null,
?context $context = null
): array {
global $DB, $USER;
if ($context === null) {
$context = context_system::instance();
}
// If user can't view all reports, limit the returned list to those reports they can see.
if (!has_capability('moodle/reportbuilder:editall', $context, $userid)) {
$reports = self::user_reports_list($userid);
[$paramprefix, $paramuserid] = database::generate_param_names(2);
[$reportselect, $params] = $DB->get_in_or_equal($reports, SQL_PARAMS_NAMED, "{$paramprefix}_", true, null);
$where = "{$reporttablealias}.id {$reportselect}";
// User can also see any reports that they can edit.
if (has_capability('moodle/reportbuilder:edit', $context, $userid)) {
$where = "({$reporttablealias}.usercreated = :{$paramuserid} OR {$where})";
$params[$paramuserid] = $userid ?? $USER->id;
}
return [$where, $params];
}
return ['1=1', []];
}
/**
* Return appropriate list of where clauses and params for given audiences
*

View File

@ -18,9 +18,6 @@ declare(strict_types=1);
namespace core_reportbuilder\local\systemreports;
use context_system;
use core_reportbuilder\local\helpers\audience;
use core_reportbuilder\local\helpers\database;
use html_writer;
use lang_string;
use moodle_url;
@ -33,6 +30,7 @@ use core_reportbuilder\local\entities\user;
use core_reportbuilder\local\filters\date;
use core_reportbuilder\local\filters\text;
use core_reportbuilder\local\filters\select;
use core_reportbuilder\local\helpers\audience;
use core_reportbuilder\local\helpers\format;
use core_reportbuilder\local\report\action;
use core_reportbuilder\local\report\column;
@ -69,11 +67,9 @@ class reports_list extends system_report {
// Select fields required for actions, permission checks, and row class callbacks.
$this->add_base_fields('rb.id, rb.name, rb.source, rb.type, rb.usercreated, rb.contextid');
// If user can't view all reports, limit the returned list to those reports they can see.
[$where, $params] = $this->filter_by_allowed_reports_sql();
if (!empty($where)) {
$this->add_base_condition_sql($where, $params);
}
// Limit the returned list to those reports the current user can access.
[$where, $params] = audience::user_reports_list_access_sql('rb');
$this->add_base_condition_sql($where, $params);
// Join user entity for "User modified" column.
$entityuser = new user();
@ -310,53 +306,4 @@ class reports_list extends system_report {
private function report_source_valid(string $source): bool {
return manager::report_source_exists($source, datasource::class) && manager::report_source_available($source);
}
/**
* Filters the list of reports to return only the ones the user has access to
*
* - A user with 'editall' capability will have access to all reports.
* - A user with 'edit' capability will have access to:
* - Those reports this user has created.
* - Those reports this user is in audience of.
* - A user with 'view' capability will have access to:
* - Those reports this user is in audience of.
*
* @return array
*/
private function filter_by_allowed_reports_sql(): array {
global $DB, $USER;
// If user can't view all reports, limit the returned list to those reports they can see.
if (!has_capability('moodle/reportbuilder:editall', context_system::instance())) {
$reports = audience::user_reports_list();
if (has_capability('moodle/reportbuilder:edit', context_system::instance())) {
// User can always see own reports and also those reports user is in audience of.
$paramuserid = database::generate_param_name();
if (empty($reports)) {
return ["rb.usercreated = :{$paramuserid}", [$paramuserid => $USER->id]];
}
$prefix = database::generate_param_name() . '_';
[$where, $params] = $DB->get_in_or_equal($reports, SQL_PARAMS_NAMED, $prefix);
$params = array_merge($params, [$paramuserid => $USER->id]);
return ["(rb.usercreated = :{$paramuserid} OR rb.id {$where})", $params];
}
// User has view capability. User can only see those reports user is in audience of.
if (empty($reports)) {
return ['1=2', []];
}
$prefix = database::generate_param_name() . '_';
[$where, $params] = $DB->get_in_or_equal($reports, SQL_PARAMS_NAMED, $prefix);
return ["rb.id {$where}", $params];
}
return ['', []];
}
}

View File

@ -0,0 +1,64 @@
<?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 core_reportbuilder_generator;
use core_user\reportbuilder\datasource\users;
/**
* Unit tests for custom report details exporter
*
* @package core_reportbuilder
* @covers \core_reportbuilder\external\custom_report_details_exporter
* @copyright 2022 Paul Holden <paulh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class custom_report_details_exporter_test extends advanced_testcase {
/**
* Test exported data structure
*/
public function test_export(): void {
global $PAGE;
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
/** @var core_reportbuilder_generator $generator */
$generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder');
$report = $generator->create_report(['name' => 'My report', 'source' => users::class]);
$exporter = new custom_report_details_exporter($report);
$export = $exporter->export($PAGE->get_renderer('core_reportbuilder'));
// The exporter outputs the persistent details, plus two other properties.
$this->assertEquals($report->get('name'), $export->name);
$this->assertEquals($report->get('source'), $export->source);
// Source name should be the name of the source.
$this->assertEquals(users::get_name(), $export->sourcename);
// We use the user exporter for the modifier of the report.
$this->assertObjectHasAttribute('modifiedby', $export);
$this->assertEquals(fullname($user), $export->modifiedby->fullname);
}
}

View File

@ -0,0 +1,92 @@
<?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\reports;
use context_system;
use core_reportbuilder_generator;
use external_api;
use externallib_advanced_testcase;
use core_reportbuilder\report_access_exception;
use core_reportbuilder\local\models\report;
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 listing reports
*
* @package core_reportbuilder
* @covers \core_reportbuilder\external\reports\listing
* @copyright 2022 Paul Holden <paulh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class listing_test extends externallib_advanced_testcase {
/**
* Text execute method
*/
public function test_execute(): void {
$this->resetAfterTest();
$this->setAdminUser();
/** @var core_reportbuilder_generator $generator */
$generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder');
// Create three reports.
$reportone = $generator->create_report(['name' => 'Report one', 'source' => users::class]);
$reporttwo = $generator->create_report(['name' => 'Report two', 'source' => users::class]);
$reportthree = $generator->create_report(['name' => 'Report three', 'source' => users::class]);
// Create second user, with audience of both report one and two.
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
$generator->create_audience(['reportid' => $reportone->get('id'), 'configdata' => []]);
$generator->create_audience(['reportid' => $reporttwo->get('id'), 'configdata' => []]);
// Switch to second user, get their report listing.
$result = listing::execute();
$result = external_api::clean_returnvalue(listing::execute_returns(), $result);
$this->assertEquals(['Report one', 'Report two'], array_column($result['reports'], 'name'));
$this->assertEmpty($result['warnings']);
}
/**
* Test execute method for a user without permission to view reports
*/
public function test_execute_access_exception(): void {
global $DB;
$this->resetAfterTest();
$userrole = $DB->get_field('role', 'id', ['shortname' => 'user'], MUST_EXIST);
assign_capability('moodle/reportbuilder:view', CAP_PROHIBIT, $userrole, context_system::instance(), true);
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
$this->expectException(report_access_exception::class);
$this->expectExceptionMessage('You cannot view this report');
listing::execute();
}
}

View File

@ -221,4 +221,71 @@ class audience_test extends advanced_testcase {
$reports = audience::user_reports_list((int) $user3->id);
$this->assertEmpty($reports);
}
/**
* Test retrieving full list of reports that user can access
*/
public function test_user_reports_list_access_sql(): void {
global $DB;
$this->resetAfterTest();
$userone = $this->getDataGenerator()->create_user();
$usertwo = $this->getDataGenerator()->create_user();
$userthree = $this->getDataGenerator()->create_user();
$userfour = $this->getDataGenerator()->create_user();
/** @var core_reportbuilder_generator $generator */
$generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder');
// Manager role gives users one and two capability to create own reports.
$managerrole = $DB->get_field('role', 'id', ['shortname' => 'manager']);
role_assign($managerrole, $userone->id, context_system::instance());
role_assign($managerrole, $usertwo->id, context_system::instance());
// Admin creates a report, no audience.
$this->setAdminUser();
$useradminreport = $generator->create_report(['name' => 'Admin report', 'source' => users::class]);
// User one creates a report, adds users two and three to audience.
$this->setUser($userone);
$useronereport = $generator->create_report(['name' => 'User one report', 'source' => users::class]);
$generator->create_audience(['reportid' => $useronereport->get('id'), 'classname' => manual::class, 'configdata' => [
'users' => [$usertwo->id, $userthree->id],
]]);
// User two creates a report, no audience.
$this->setUser($usertwo);
$usertworeport = $generator->create_report(['name' => 'User two report', 'source' => users::class]);
// Admin user sees all reports.
$this->setAdminUser();
[$where, $params] = audience::user_reports_list_access_sql('r');
$reports = $DB->get_fieldset_sql("SELECT r.id FROM {reportbuilder_report} r WHERE {$where}", $params);
$this->assertEqualsCanonicalizing([
$useradminreport->get('id'),
$useronereport->get('id'),
$usertworeport->get('id'),
], $reports);
// User one sees only the report they created.
[$where, $params] = audience::user_reports_list_access_sql('r', (int) $userone->id);
$reports = $DB->get_fieldset_sql("SELECT r.id FROM {reportbuilder_report} r WHERE {$where}", $params);
$this->assertEquals([$useronereport->get('id')], $reports);
// User two see the report they created and the one they are in the audience of.
[$where, $params] = audience::user_reports_list_access_sql('r', (int) $usertwo->id);
$reports = $DB->get_fieldset_sql("SELECT r.id FROM {reportbuilder_report} r WHERE {$where}", $params);
$this->assertEqualsCanonicalizing([$useronereport->get('id'), $usertworeport->get('id')], $reports);
// User three sees the report they are in the audience of.
[$where, $params] = audience::user_reports_list_access_sql('r', (int) $userthree->id);
$reports = $DB->get_fieldset_sql("SELECT r.id FROM {reportbuilder_report} r WHERE {$where}", $params);
$this->assertEquals([$useronereport->get('id')], $reports);
// User four sees no reports.
[$where, $params] = audience::user_reports_list_access_sql('r', (int) $userfour->id);
$reports = $DB->get_fieldset_sql("SELECT r.id FROM {reportbuilder_report} r WHERE {$where}", $params);
$this->assertEmpty($reports);
}
}

View File

@ -10,6 +10,7 @@ Information provided here is intended especially for developers.
* The external `core_reportbuilder_filters_reset` method now accepts an optional `parameters` argument, required by
some system reports
* New external methods for retrieving custom report data:
- `core_reportbuilder_list_reports`
- `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.
@ -35,6 +36,7 @@ Information provided here is intended especially for developers.
* New method `get_default_condition_values()` in base datasource class, to be overridden by sources that wish to
define default values for conditions upon report creation.
* New methods `get_identity_[column|filter]` in user entity, for retrieving user identity field report elements
* New method `user_reports_list_access_sql` in audience helper for retrieving list of all reports for given user
* New report filter types:
- `category` for reports containing course categories
- `tags` for reports containing entities with support for core_tag API

View File

@ -29,7 +29,7 @@
defined('MOODLE_INTERNAL') || die();
$version = 2022093000.00; // YYYYMMDD = weekly release date of this DEV branch.
$version = 2022093000.01; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
$release = '4.1dev (Build: 20220930)'; // Human-friendly version name