diff --git a/cohort/classes/reportbuilder/audience/cohortmember.php b/cohort/classes/reportbuilder/audience/cohortmember.php new file mode 100644 index 00000000000..d2e83318f15 --- /dev/null +++ b/cohort/classes/reportbuilder/audience/cohortmember.php @@ -0,0 +1,175 @@ +. + +declare(strict_types=1); + +namespace core_cohort\reportbuilder\audience; + +use context; +use context_system; +use core_course_category; +use stdClass; +use core_reportbuilder\local\audiences\base; +use core_reportbuilder\local\helpers\database; +use MoodleQuickForm; + +/** + * The backend class for Cohort member audience type + * + * @package core_reportbuilder + * @copyright 2021 David Matamoros + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cohortmember extends base { + + /** + * Adds audience's elements to the given mform + * + * @param MoodleQuickForm $mform The form to add elements to + */ + public function get_config_form(MoodleQuickForm $mform): void { + $cohorts = self::get_cohorts(); + $mform->addElement('autocomplete', 'cohorts', get_string('selectfromcohort', 'cohort'), + $cohorts, ['multiple' => true]); + $mform->addRule('cohorts', null, 'required', null, 'client'); + } + + /** + * Helps to build SQL to retrieve users that matches the current report audience + * + * @param string $usertablealias + * @return array array of three elements [$join, $where, $params] + */ + public function get_sql(string $usertablealias): array { + global $DB; + + $cm = database::generate_alias(); + $cohorts = $this->get_configdata()['cohorts']; + $prefix = database::generate_param_name() . '_'; + [$insql, $inparams] = $DB->get_in_or_equal($cohorts, SQL_PARAMS_NAMED, $prefix); + + $join = "JOIN {cohort_members} {$cm} + ON ({$cm}.userid = {$usertablealias}.id)"; + + return [$join, "{$cm}.cohortid " . $insql, $inparams]; + } + + /** + * Return user friendly name of this audience type + * + * @return string + */ + public function get_name(): string { + return get_string('memberofcohort', 'cohort'); + } + + /** + * Return the description for the audience. + * + * @return string + */ + public function get_description(): string { + global $DB; + + $cohortlist = []; + + $cohortids = $this->get_configdata()['cohorts']; + $cohorts = $DB->get_records_list('cohort', 'id', $cohortids, 'name'); + foreach ($cohorts as $cohort) { + $cohortlist[] = format_string($cohort->name, true, ['context' => $cohort->contextid, 'escape' => false]); + } + + return $this->format_description_for_multiselect($cohortlist); + } + + /** + * If the current user is able to add this audience. + * + * @return bool + */ + public function user_can_add(): bool { + // Check system context first. + if (has_capability('moodle/cohort:view', context_system::instance())) { + return true; + } + // If there is at least one category with given permissions, user can add. + return !empty(core_course_category::make_categories_list('moodle/cohort:view')); + } + + /** + * Returns if this audience type is available for the user + * + * Check if there are available cohorts in the system for this user to use. + * + * @return bool + */ + public function is_available(): bool { + return !empty(self::get_cohorts()); + } + + /** + * If the current user is able to edit this audience. + * + * @return bool + */ + public function user_can_edit(): bool { + global $DB; + + $canedit = true; + $cohortids = $this->get_configdata()['cohorts']; + $cohorts = $DB->get_records_list('cohort', 'id', $cohortids); + foreach ($cohorts as $cohort) { + $context = context::instance_by_id($cohort->contextid, MUST_EXIST); + $canedit = $canedit && has_capability('moodle/cohort:view', $context); + if ($canedit === false) { + break; + } + } + + return $canedit; + } + + /** + * Cohorts selector. + * + * @return array + */ + private static function get_cohorts(): array { + global $CFG; + + require_once($CFG->dirroot.'/cohort/lib.php'); + + $cohortslist = []; + + // Search cohorts user can view. + $usercohorts = cohort_get_all_cohorts(0, 0); + + // The previous method doesn't check cohorts on system context. + $syscontext = context_system::instance(); + $cohorts = array_filter($usercohorts['cohorts'], static function(stdClass $cohort) use ($syscontext): bool { + return ($cohort->contextid != $syscontext->id) || has_capability('moodle/cohort:view', $syscontext); + }); + + foreach ($cohorts as $cohort) { + $cohortslist[$cohort->id] = format_string($cohort->name, true, [ + 'context' => $cohort->contextid, + 'escape' => false, + ]); + } + + return $cohortslist; + } +} diff --git a/cohort/tests/reportbuilder/audience/cohortmember_test.php b/cohort/tests/reportbuilder/audience/cohortmember_test.php new file mode 100644 index 00000000000..bfd7620cb2d --- /dev/null +++ b/cohort/tests/reportbuilder/audience/cohortmember_test.php @@ -0,0 +1,193 @@ +. + +declare(strict_types=1); + +namespace core_cohort\reportbuilder\audience; + +use advanced_testcase; +use context; +use core_reportbuilder_generator; +use core_user\reportbuilder\datasource\users; + +/** + * Unit tests for cohort member report audience type + * + * @package core_reportbuilder + * @covers \core_cohort\reportbuilder\audience\cohortmember + * @copyright 2021 David Matamoros + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cohortmember_test extends advanced_testcase { + + /** + * Test that this audience type description is generated correctly + */ + public function test_get_description(): 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, + 'default' => false, + ]); + + $cohort = self::getDataGenerator()->create_cohort(); + $audience = cohortmember::create($report->get('id'), ['cohorts' => [$cohort->id]]); + $this->assertEquals($cohort->name, $audience->get_description()); + } + + /** + * Test if user can add this audience type to the report + */ + public function test_user_can_add(): 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, + 'default' => false, + ]); + + // Admin user. + self::setAdminUser(); + + $cohort = self::getDataGenerator()->create_cohort(); + $context = context::instance_by_id($cohort->contextid); + $audience = cohortmember::create($report->get('id'), ['cohorts' => [$cohort->id]]); + + $this->assertTrue($audience->user_can_add()); + + // Non-priveleged user. + $user = self::getDataGenerator()->create_user(); + self::setUser($user); + $this->assertFalse($audience->user_can_add()); + + // Grant priveleges to user (moodle/cohort:view). + $roleid = create_role('Dummy role', 'dummyrole', 'dummy role description'); + assign_capability('moodle/cohort:view', CAP_ALLOW, $roleid, $context->id); + role_assign($roleid, $user->id, $context->id); + $this->assertTrue($audience->user_can_add()); + } + + /** + * Test if user can edit this audience type + */ + public function test_user_can_edit(): 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, + 'default' => false, + ]); + + $cohort = self::getDataGenerator()->create_cohort(); + $context = context::instance_by_id($cohort->contextid); + $audience = cohortmember::create($report->get('id'), ['cohorts' => [$cohort->id]]); + + // Admin user. + self::setAdminUser(); + $this->assertTrue($audience->user_can_edit()); + + // Non-priveleged user. + $user = self::getDataGenerator()->create_user(); + self::setUser($user); + $this->assertFalse($audience->user_can_edit()); + + // Grant priveleges to user (moodle/cohort:view). + $roleid = create_role('Dummy role', 'dummyrole', 'dummy role description'); + assign_capability('moodle/cohort:view', CAP_ALLOW, $roleid, $context->id); + role_assign($roleid, $user->id, $context->id); + $this->assertTrue($audience->user_can_edit()); + } + + /** + * Test that sql generated is correct + */ + public function test_get_sql(): void { + global $DB; + $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, + 'default' => false, + ]); + + $cohort = self::getDataGenerator()->create_cohort(); + + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $user3 = $this->getDataGenerator()->create_user(); + + // Add user1 into cohort. + cohort_add_member($cohort->id, $user1->id); + // Add user3 into cohort. + cohort_add_member($cohort->id, $user3->id); + + $audience = cohortmember::create($report->get('id'), ['cohorts' => [$cohort->id]]); + + [$join, $where, $params] = $audience->get_sql('u'); + $query = 'SELECT u.* FROM {user} u ' . $join . ' WHERE ' . $where; + $records = $DB->get_records_sql($query, $params); + + $this->assertEqualsCanonicalizing([$user1->id, $user3->id], array_column($records, 'id')); + } + + /** + * Test if this audience type is available to use + */ + public function test_is_available(): void { + $this->resetAfterTest(); + + /** @var core_reportbuilder_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); + + self::setAdminUser(); + + // Check with no cohorts available in the system. + $report = $generator->create_report([ + 'name' => 'My report', + 'source' => users::class, + 'default' => false, + ]); + $audience = cohortmember::create($report->get('id'), ['cohorts' => []]); + $this->assertFalse($audience->is_available()); + + // Check with cohorts available in the system. + self::getDataGenerator()->create_cohort(); + $report = $generator->create_report([ + 'name' => 'My report', + 'source' => users::class, + 'default' => false, + ]); + $audience2 = cohortmember::create($report->get('id'), ['cohorts' => []]); + $this->assertTrue($audience2->is_available()); + } +} diff --git a/lang/en/cache.php b/lang/en/cache.php index f7c547eb2da..421b141932b 100644 --- a/lang/en/cache.php +++ b/lang/en/cache.php @@ -81,6 +81,7 @@ $string['cachedef_postprocessedcss'] = 'Post processed CSS'; $string['cachedef_tagindexbuilder'] = 'Search results for tagged items'; $string['cachedef_questiondata'] = 'Question definitions'; $string['cachedef_recommendation_favourite_course_content_items'] = 'Recommendation of course content items'; +$string['cachedef_reportbuilder_allowed_reports'] = 'Users allowed reports according to audience'; $string['cachedef_repositories'] = 'Repositories instances data'; $string['cachedef_roledefs'] = 'Role definitions'; $string['cachedef_grade_categories'] = 'Grade category queries'; diff --git a/lang/en/langconfig.php b/lang/en/langconfig.php index 4d88b365b75..9b748db8f1c 100644 --- a/lang/en/langconfig.php +++ b/lang/en/langconfig.php @@ -31,6 +31,7 @@ $string['iso6391'] = 'en'; $string['iso6392'] = 'eng'; $string['labelsep'] = ': '; $string['listsep'] = ','; +$string['listitemssep'] = ', '; $string['locale'] = 'en_AU.UTF-8'; $string['localecldr'] = 'en-AU'; $string['localewin'] = 'English_Australia.1252'; diff --git a/lang/en/reportbuilder.php b/lang/en/reportbuilder.php index f089c47d1a9..3c750d29809 100644 --- a/lang/en/reportbuilder.php +++ b/lang/en/reportbuilder.php @@ -24,6 +24,7 @@ $string['actions'] = 'Actions'; $string['addcolumn'] = 'Add column \'{$a}\''; +$string['addusers'] = 'Add users manually'; $string['aggregatecolumn'] = 'Aggregate column \'{$a}\''; $string['aggregationavg'] = 'Average'; $string['aggregationcount'] = 'Count'; @@ -35,6 +36,8 @@ $string['aggregationmin'] = 'Minimum'; $string['aggregationnone'] = 'No aggregation'; $string['aggregationpercent'] = 'Percentage'; $string['aggregationsum'] = 'Sum'; +$string['allsiteusers'] = 'All site users'; +$string['allusers'] = 'All users'; $string['apply'] = 'Apply'; $string['columnadded'] = 'Added column \'{$a}\''; $string['columnaggregated'] = 'Aggregated column \'{$a}\''; @@ -115,8 +118,10 @@ $string['filters'] = 'Filters'; $string['filters_help'] = 'Report filters allow users viewing the report to further limit the data being displayed, by selecting their own values for each filter. Filter values are stored on a per-user basis.'; $string['filtersreset'] = 'Filters reset'; $string['filterstartswith'] = 'Starts with'; +$string['hassystemrole'] = 'Assigned system role'; $string['includedefaultsetup'] = 'Include default setup'; $string['includedefaultsetup_help'] = 'Populate report with default layout as defined by the selected source. These include pre-defined columns, filters and conditions.'; +$string['manuallyaddedusers'] = 'Manually added users'; $string['movecolumn'] = 'Move column \'{$a}\''; $string['movecondition'] = 'Move condition \'{$a}\''; $string['movefilter'] = 'Move filter \'{$a}\''; diff --git a/lib/db/caches.php b/lib/db/caches.php index 5fabc3bbfb3..e64739d5502 100644 --- a/lib/db/caches.php +++ b/lib/db/caches.php @@ -523,4 +523,13 @@ $definitions = array( 'changesincourse', ], ], + + // Users allowed reports according to audience. + 'reportbuilder_allowed_reports' => [ + 'mode' => cache_store::MODE_APPLICATION, + 'simplekeys' => true, + 'simpledata' => true, + 'staticacceleration' => true, + 'ttl' => 1800, + ], ); diff --git a/reportbuilder/classes/local/audiences/base.php b/reportbuilder/classes/local/audiences/base.php new file mode 100644 index 00000000000..c076bcd2bc3 --- /dev/null +++ b/reportbuilder/classes/local/audiences/base.php @@ -0,0 +1,273 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\local\audiences; + +use core_plugin_manager; +use MoodleQuickForm; +use stdClass; +use core\output\notification; +use core_reportbuilder\local\models\audience; +use core_reportbuilder\report_access_exception; + +/** + * Audience base class + * + * @package core_reportbuilder + * @copyright 2021 David Matamoros + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class base { + + /** @var int Maximim number of multi-select elements to show in description, before appending "plus X more" */ + private const MULTI_SELECT_LIMIT = 5; + + /** @var audience The persistent object associated with this audience */ + protected $audience; + + /** + * Protected constructor, please use the static instance method. + */ + protected function __construct() { + } + + /** + * Loads an existing instance of audience with persistent + * + * @param int $id + * @param null|stdClass $record + * @return self|null + */ + final public static function instance(int $id = 0, ?stdClass $record = null): ?self { + $persistent = new audience($id, $record); + // Needed for get_audience_types() method. + if (!$classname = $persistent->get('classname')) { + // Use the called class name. + $classname = get_called_class(); + $persistent->set('classname', $classname); + } + + // Check if audience type class still exists in the system. + if (!class_exists($classname)) { + return null; + } + + $instance = new $classname(); + $instance->audience = $persistent; + return $instance; + } + + /** + * Creates a new audience and saves it to database + * + * @param int $reportid + * @param array $configdata + * @return self + */ + final public static function create(int $reportid, array $configdata): self { + $record = new stdClass(); + $record->reportid = $reportid; + $record->classname = get_called_class(); + $record->configdata = json_encode($configdata); + $instance = self::instance(0, $record); + $instance->audience->save(); + return $instance; + } + + /** + * Helps to build SQL to retrieve users that matches the current audience + * + * Implementations must use api::generate_alias() for table/column aliases + * and api::generate_param_name() for named parameters + * + * @param string $usertablealias + * @return array array of three elements [$join, $where, $params] + */ + abstract public function get_sql(string $usertablealias): array; + + /** + * Returns string for audience category. + * + * @return string + */ + final public function get_category(): string { + [$component] = explode('\\', get_class($this)); + + if ($plugininfo = core_plugin_manager::instance()->get_plugin_info($component)) { + return $plugininfo->displayname; + } + + return get_string('site'); + } + + /** + * If the current user is able to add this audience type + * + * @return bool + */ + abstract public function user_can_add(): bool; + + /** + * If the current user is able to edit this audience type + * + * @return bool + */ + abstract public function user_can_edit(): bool; + + /** + * If the current user is able to use this audience type + * + * This method needs to return true if audience type is available to user for + * reasons other than permission check, which is done in {@see user_can_add}. + * (e.g. user can add cohort audience type only if there is at least one cohort + * they can access). + * + * @return bool + */ + public function is_available(): bool { + return true; + } + + /** + * Return user friendly name of the audience type + * + * @return string + */ + abstract public function get_name(): string; + + /** + * Return the description of this audience type + * + * @return string + */ + abstract public function get_description(): string; + + /** + * Helper to format descriptions for audience types that may contain many selected elements, limiting number show according + * to {@see MULTI_SELECT_LIMIT} constant value + * + * @param array $elements + * @return string + */ + protected function format_description_for_multiselect(array $elements): string { + global $OUTPUT; + + // Warn user if there are no elements (because they may no longer exist). + $elementcount = count($elements); + if ($elementcount === 0) { + $notification = new notification(get_string('nothingtodisplay'), notification::NOTIFY_WARNING); + return $OUTPUT->render($notification); + } + + $listseparator = get_string('listitemssep', 'langconfig'); + if ($elementcount > self::MULTI_SELECT_LIMIT) { + $elements = array_slice($elements, 0, self::MULTI_SELECT_LIMIT); + + // Append overflow element. + $elementoverflow = $elementcount - self::MULTI_SELECT_LIMIT; + $params = [ + 'elements' => implode($listseparator, $elements), + 'morecount' => $elementoverflow, + ]; + $description = get_string('audiencemultiselectpostfix', 'core_reportbuilder', $params); + } else { + $description = implode($listseparator, $elements); + } + + return $description; + } + + /** + * Adds audience-specific form elements + * + * @param MoodleQuickForm $mform The form to add elements to + */ + abstract public function get_config_form(MoodleQuickForm $mform): void; + + /** + * Validates the configform of the condition. + * + * @param array $data Data from the form + * @return array Array with errors for each element + */ + public function validate_config_form(array $data): array { + return []; + } + + /** + * Returns configdata as an associative array + * + * @return array decoded configdata + */ + final public function get_configdata(): array { + return json_decode($this->audience->get('configdata'), true); + } + + /** + * Update configdata in audience persistent + * + * @param array $configdata + */ + final public function update_configdata(array $configdata): void { + $this->audience->set('configdata', json_encode($configdata)); + $this->audience->save(); + } + + /** + * Returns $configdata from form data suitable for use in DB record. + * + * @param stdClass $data data obtained from $mform->get_data() + * @return array $configdata + */ + final public static function retrieve_configdata(stdClass $data): array { + $configdata = (array) $data; + $invalidkeys = array_fill_keys(['id', 'reportid', 'classname'], ''); + return array_diff_key($configdata, $invalidkeys); + } + + /** + * Return audience persistent. + * + * @return audience + */ + public function get_persistent(): audience { + return $this->audience; + } + + /** + * Require current user is able to add this audience type + * + * @throws report_access_exception + */ + public function require_user_can_add(): void { + if (!$this->user_can_add()) { + throw new report_access_exception('errorreportedit'); + } + } + + /** + * Require current user is able to edit this audience type + * + * @throws report_access_exception + */ + public function require_user_can_edit(): void { + if (!$this->user_can_edit()) { + throw new report_access_exception('errorreportedit'); + } + } +} diff --git a/reportbuilder/classes/local/helpers/audience.php b/reportbuilder/classes/local/helpers/audience.php new file mode 100644 index 00000000000..d8bcf2c8335 --- /dev/null +++ b/reportbuilder/classes/local/helpers/audience.php @@ -0,0 +1,243 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\local\helpers; + +use cache; +use core_collator; +use core_component; +use core_plugin_manager; +use core_reportbuilder\local\audiences\base; +use core_reportbuilder\local\models\audience as audience_model; + +/** + * Class containing report audience helper methods + * + * @package core_reportbuilder + * @copyright 2021 David Matamoros + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class audience { + + /** + * Return audience base records for a given report + * + * @param int $reportid + * @return base[] + */ + public static function get_base_records(int $reportid): array { + $records = audience_model::get_records(['reportid' => $reportid], 'id'); + return array_map(function ($record) { + return base::instance(0, $record->to_record()); + }, $records); + } + + /** + * Returns list of reports that the specified user can access. Note this is potentially very expensive to calculate if a + * 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 + */ + public static function get_allowed_reports(?int $userid = null): array { + global $USER, $DB; + + $userid = $userid ?: (int) $USER->id; + + // Prepare cache, if we previously stored the users allowed reports then return that. + $cache = cache::make('core', 'reportbuilder_allowed_reports'); + $cachedreports = $cache->get($userid); + if ($cachedreports !== false) { + return $cachedreports; + } + + $allowedreports = []; + $reportaudiences = []; + + // Retrieve all audiences and group them by report for convenience. + $audiences = audience_model::get_records(); + foreach ($audiences as $audience) { + $reportaudiences[$audience->get('reportid')][] = $audience; + } + + foreach ($reportaudiences as $reportid => $audiences) { + + // Generate audience SQL based on those for the current report. + [$wheres, $params] = self::user_audience_sql($audiences); + $allwheres = implode(' OR ', $wheres); + + $paramuserid = database::generate_param_name(); + $params[$paramuserid] = $userid; + + $sql = "SELECT DISTINCT(u.id) + FROM {user} u + WHERE ({$allwheres}) + AND u.id = :{$paramuserid}"; + + // If we have a matching record, user can view the report. + if ($DB->record_exists_sql($sql, $params)) { + $allowedreports[] = $reportid; + } + } + + // Store users allowed reports in cache. + $cache->set($userid, $allowedreports); + + return $allowedreports; + } + + /** + * Purge the audience cache of allowed reports + */ + public static function purge_caches(): void { + cache::make('core', 'reportbuilder_allowed_reports')->purge(); + } + + /** + * Generate SQL select clause and params for selecting reports specified user can access + * + * @param string $reporttablealias + * @param int|null $userid User ID to check, or the current user if omitted + * @return array + */ + public static function user_reports_list_sql(string $reporttablealias, ?int $userid = null): array { + global $DB; + + $allowedreports = self::get_allowed_reports($userid); + + if (empty($allowedreports)) { + return ['1=0', []]; + } + + // Get all sql audiences. + $prefix = database::generate_param_name() . '_'; + [$select, $params] = $DB->get_in_or_equal($allowedreports, SQL_PARAMS_NAMED, $prefix); + $sql = "{$reporttablealias}.id {$select}"; + + return [$sql, $params]; + } + + /** + * Return list of report ID's specified user can access + * + * @param int|null $userid User ID to check, or the current user if omitted + * @return int[] + */ + public static function user_reports_list(?int $userid = null): array { + global $DB; + + [$select, $params] = self::user_reports_list_sql('rb', $userid); + $sql = "SELECT rb.id + FROM {reportbuilder_report} rb + WHERE {$select}"; + + return $DB->get_fieldset_sql($sql, $params); + } + + /** + * Return appropriate list of where clauses and params for given audiences + * + * @param audience_model[] $audiences + * @param string $usertablealias + * @return array[] [$wheres, $params] + */ + public static function user_audience_sql(array $audiences, string $usertablealias = 'u'): array { + $wheres = $params = []; + + foreach ($audiences as $audience) { + if ($instance = base::instance(0, $audience->to_record())) { + $instancetablealias = database::generate_alias(); + [$instancejoin, $instancewhere, $instanceparams] = $instance->get_sql($instancetablealias); + + $wheres[] = "{$usertablealias}.id IN ( + SELECT {$instancetablealias}.id + FROM {user} {$instancetablealias} + {$instancejoin} + WHERE {$instancewhere} + )"; + $params += $instanceparams; + } + } + + return [$wheres, $params]; + } + + /** + * Returns the list of audiences types in the system. + * + * @return array + */ + private static function get_audience_types(): array { + $sources = []; + + $audiences = core_component::get_component_classes_in_namespace(null, 'reportbuilder\\audience'); + foreach ($audiences as $class => $path) { + $audienceclass = $class::instance(); + if (is_subclass_of($class, base::class) && $audienceclass->user_can_add()) { + [$component] = explode('\\', $class); + + if ($plugininfo = core_plugin_manager::instance()->get_plugin_info($component)) { + $componentname = $plugininfo->displayname; + } else { + $componentname = get_string('site'); + } + + $sources[$componentname][$class] = $audienceclass->get_name(); + } + } + + return $sources; + } + + /** + * Get all the audiences types the current user can add to, organised by categories. + * + * @return array + */ + public static function get_all_audiences_menu_types(): array { + $menucardsarray = []; + $notavailablestr = get_string('notavailable', 'moodle'); + + $audiencetypes = self::get_audience_types(); + foreach ($audiencetypes as $categoryname => $audience) { + $menucards = [ + 'name' => $categoryname, + ]; + + foreach ($audience as $classname => $name) { + $class = $classname::instance(); + $title = $class->is_available() ? get_string('addaudience', 'core_reportbuilder', $class->get_name()) : + $notavailablestr; + $menucard['title'] = $title; + $menucard['name'] = $class->get_name(); + $menucard['disabled'] = !$class->is_available(); + $menucard['identifier'] = get_class($class); + $menucard['action'] = 'add-audience'; + $menucards['items'][] = $menucard; + } + + // Order audience types on each category alphabetically. + core_collator::asort_array_of_arrays_by_key($menucards['items'], 'name'); + $menucards['items'] = array_values($menucards['items']); + + $menucardsarray[] = $menucards; + } + + return $menucardsarray; + } +} diff --git a/reportbuilder/classes/reportbuilder/audience/allusers.php b/reportbuilder/classes/reportbuilder/audience/allusers.php new file mode 100644 index 00000000000..0deff95a5f1 --- /dev/null +++ b/reportbuilder/classes/reportbuilder/audience/allusers.php @@ -0,0 +1,93 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\reportbuilder\audience; + +use context_system; +use core_reportbuilder\local\audiences\base; +use core_reportbuilder\local\helpers\database; +use MoodleQuickForm; + +/** + * The backend class for All users audience type + * + * @package core_reportbuilder + * @copyright 2021 David Matamoros + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class allusers extends base { + + /** + * Adds audience's elements to the given mform + * + * @param MoodleQuickForm $mform The form to add elements to + */ + public function get_config_form(MoodleQuickForm $mform): void { + $mform->addElement('static', 'allsiteusers', get_string('allsiteusers', 'core_reportbuilder')); + } + + /** + * Helps to build SQL to retrieve users that matches the current report audience + * + * @param string $usertablealias + * @return array array of three elements [$join, $where, $params] + */ + public function get_sql(string $usertablealias): array { + global $CFG; + + $guestuser = database::generate_param_name(); + return ['', "$usertablealias.suspended = 0 AND $usertablealias.deleted = 0 AND $usertablealias.id <> :{$guestuser}", + [$guestuser => $CFG->siteguest]]; + } + + /** + * Return user friendly name of this audience type + * + * @return string + */ + public function get_name(): string { + return get_string('allusers', 'core_reportbuilder'); + } + + /** + * Return the description for the audience. + * + * @return string + */ + public function get_description(): string { + return get_string('allsiteusers', 'core_reportbuilder'); + } + + /** + * If the current user is able to add this audience. + * + * @return bool + */ + public function user_can_add(): bool { + return has_capability('moodle/user:viewalldetails', context_system::instance()); + } + + /** + * If the current user is able to edit this audience. + * + * @return bool + */ + public function user_can_edit(): bool { + return has_capability('moodle/user:viewalldetails', context_system::instance()); + } +} diff --git a/reportbuilder/classes/reportbuilder/audience/manual.php b/reportbuilder/classes/reportbuilder/audience/manual.php new file mode 100644 index 00000000000..baee0317b53 --- /dev/null +++ b/reportbuilder/classes/reportbuilder/audience/manual.php @@ -0,0 +1,120 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\reportbuilder\audience; + +use context_system; +use core_reportbuilder\local\audiences\base; +use core_reportbuilder\local\helpers\database; +use core_user; +use MoodleQuickForm; + +/** + * The backend class for Manually added users audience type + * + * @package core_reportbuilder + * @copyright 2021 David Matamoros + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class manual extends base { + + /** + * Adds audience's elements to the given mform + * + * @param MoodleQuickForm $mform The form to add elements to + */ + public function get_config_form(MoodleQuickForm $mform): void { + // Users selector. + $options = [ + 'ajax' => 'core_user/form_user_selector', + 'multiple' => true, + 'valuehtmlcallback' => function($userid) { + $user = core_user::get_user($userid); + return fullname($user, has_capability('moodle/site:viewfullnames', context_system::instance())); + } + ]; + + $mform->addElement('autocomplete', 'users', get_string('addusers', 'core_reportbuilder'), [], $options); + $mform->addRule('users', null, 'required', null, 'client'); + } + + /** + * Helps to build SQL to retrieve users that matches the current report audience + * + * @param string $usertablealias + * @return array array of three elements [$join, $where, $params] + */ + public function get_sql(string $usertablealias): array { + global $DB; + + $users = $this->get_configdata()['users']; + $prefix = database::generate_param_name() . '_'; + [$insql, $inparams] = $DB->get_in_or_equal($users, SQL_PARAMS_NAMED, $prefix); + + return ['', "{$usertablealias}.id $insql", $inparams]; + } + + /** + * Return user friendly name of this audience type + * + * @return string + */ + public function get_name(): string { + return get_string('manuallyaddedusers', 'core_reportbuilder'); + } + + /** + * Return the description for the audience. + * + * @return string + */ + public function get_description(): string { + global $DB; + + $canviewfullnames = has_capability('moodle/site:viewfullnames', context_system::instance()); + + $userslist = []; + + $userids = $this->get_configdata()['users']; + [$sort] = users_order_by_sql(); + $users = $DB->get_records_list('user', 'id', $userids, $sort); + foreach ($users as $user) { + $userslist[] = fullname($user, $canviewfullnames); + } + + return $this->format_description_for_multiselect($userslist); + } + + /** + * If the current user is able to add this audience. + * + * @return bool + */ + public function user_can_add(): bool { + return has_capability('moodle/user:viewalldetails', context_system::instance()); + } + + /** + * If the current user is able to edit this audience. + * + * @return bool + */ + public function user_can_edit(): bool { + return has_capability('moodle/user:viewalldetails', context_system::instance()); + } +} diff --git a/reportbuilder/classes/reportbuilder/audience/systemrole.php b/reportbuilder/classes/reportbuilder/audience/systemrole.php new file mode 100644 index 00000000000..315aa2d7b43 --- /dev/null +++ b/reportbuilder/classes/reportbuilder/audience/systemrole.php @@ -0,0 +1,132 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\reportbuilder\audience; + +use context_system; +use core_reportbuilder\local\audiences\base; +use core_reportbuilder\local\helpers\database; +use MoodleQuickForm; + +/** + * The backend class for Has system role audience type + * + * @package core_reportbuilder + * @copyright 2021 David Matamoros + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class systemrole extends base { + + /** + * Adds audience's elements to the given mform + * + * @param MoodleQuickForm $mform The form to add elements to + */ + public function get_config_form(MoodleQuickForm $mform): void { + $roles = get_assignable_roles(context_system::instance(), ROLENAME_ALIAS); + + $mform->addElement('autocomplete', 'roles', get_string('selectrole', 'role'), $roles, ['multiple' => true]); + $mform->addRule('roles', null, 'required', null, 'client'); + } + + /** + * Helps to build SQL to retrieve users that matches the current audience + * + * @param string $usertablealias + * @return array array of three elements [$join, $where, $params] + */ + public function get_sql(string $usertablealias): array { + global $DB; + + $roles = $this->get_configdata()['roles']; + $prefix = database::generate_param_name() . '_'; + [$insql, $inparams] = $DB->get_in_or_equal($roles, SQL_PARAMS_NAMED, $prefix); + + $contextid = database::generate_param_name(); + $ra = database::generate_alias(); + $ctx = database::generate_alias(); + + $join = " + JOIN {role_assignments} {$ra} ON {$ra}.userid = {$usertablealias}.id + JOIN {context} {$ctx} ON {$ctx}.id = {$ra}.contextid"; + + $where = "{$ra}.contextid = :{$contextid} AND {$ra}.roleid {$insql}"; + + return [$join, $where, $inparams + [$contextid => context_system::instance()->id]]; + } + + /** + * Return user friendly name of this audience type + * + * @return string + */ + public function get_name(): string { + return get_string('hassystemrole', 'core_reportbuilder'); + } + + /** + * Return the description for the audience. + * + * @return string + */ + public function get_description(): string { + global $DB; + $rolesids = $this->get_configdata()['roles']; + $roles = $DB->get_records_list('role', 'id', $rolesids, 'sortorder'); + $rolesfixed = role_fix_names($roles, context_system::instance(), ROLENAME_ALIAS, true); + return $this->format_description_for_multiselect($rolesfixed); + } + + /** + * If the current user is able to add this audience. + * + * @return bool + */ + public function user_can_add(): bool { + // Check if user is able to assign any role from the system context. + $roles = get_assignable_roles(context_system::instance(), ROLENAME_ALIAS); + if (empty($roles)) { + return false; + } + + return true; + } + + /** + * If the current user is able to edit this audience. + * + * @return bool + */ + public function user_can_edit(): bool { + global $DB; + + // Check if user can assign all saved role types on this audience instance. + $roleids = $this->get_configdata()['roles']; + $roles = get_assignable_roles(context_system::instance(), ROLENAME_ALIAS); + + // Check that all saved roles still exist. + [$insql, $inparams] = $DB->get_in_or_equal($roleids, SQL_PARAMS_NAMED); + $records = $DB->get_records_select('role', "id $insql", $inparams); + + if (!empty(array_diff(array_keys($records), array_keys($roles)))) { + return false; + } + + return true; + } +} diff --git a/reportbuilder/tests/generator/behat_core_reportbuilder_generator.php b/reportbuilder/tests/generator/behat_core_reportbuilder_generator.php index 3ee06cc5c66..c9c08e8ea38 100644 --- a/reportbuilder/tests/generator/behat_core_reportbuilder_generator.php +++ b/reportbuilder/tests/generator/behat_core_reportbuilder_generator.php @@ -71,6 +71,16 @@ class behat_core_reportbuilder_generator extends behat_generator_base { 'report' => 'reportid', ], ], + 'Audience' => [ + 'datagenerator' => 'audience', + 'required' => [ + 'report', + 'configdata', + ], + 'switchids' => [ + 'report' => 'reportid', + ], + ], ]; } diff --git a/reportbuilder/tests/generator/lib.php b/reportbuilder/tests/generator/lib.php index 20de5d2f663..1d01a53be5c 100644 --- a/reportbuilder/tests/generator/lib.php +++ b/reportbuilder/tests/generator/lib.php @@ -20,6 +20,7 @@ use core_reportbuilder\local\helpers\report as helper; use core_reportbuilder\local\models\column; use core_reportbuilder\local\models\filter; use core_reportbuilder\local\models\report; +use core_reportbuilder\local\audiences\base as audience_base; /** * Report builder test generator @@ -112,4 +113,30 @@ class core_reportbuilder_generator extends component_generator_base { return helper::add_report_condition($record['reportid'], $record['uniqueidentifier']); } + + /** + * Create report audience + * + * @param array|stdClass $record + * @return audience_base + * @throws coding_exception + */ + public function create_audience($record): audience_base { + $record = (array) $record; + + // Required properties. + if (!array_key_exists('reportid', $record)) { + throw new coding_exception('Record must contain \'reportid\' property'); + } + if (!array_key_exists('configdata', $record)) { + throw new coding_exception('Record must contain \'configdata\' property'); + } + + // Default to all users if not specified, for convenience. + /** @var audience_base $classname */ + $classname = $record['classname'] ?? + \core_reportbuilder\reportbuilder\audience\allusers::class; + + return ($classname)::create($record['reportid'], $record['configdata']); + } } diff --git a/reportbuilder/tests/local/helpers/audience_test.php b/reportbuilder/tests/local/helpers/audience_test.php new file mode 100644 index 00000000000..657d733c69c --- /dev/null +++ b/reportbuilder/tests/local/helpers/audience_test.php @@ -0,0 +1,239 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\local\helpers; + +use advanced_testcase; +use context_system; +use core_reportbuilder_generator; +use core_reportbuilder\reportbuilder\audience\manual; +use core_user\reportbuilder\datasource\users; + +/** + * Unit tests for audience helper + * + * @package core_reportbuilder + * @covers \core_reportbuilder\local\helpers\audience + * @copyright 2021 David Matamoros + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class audience_test extends advanced_testcase { + + /** + * Test reports list is empty for a normal user without any audience records configured + */ + public function test_reports_list_no_access(): void { + $this->resetAfterTest(); + + $reports = audience::user_reports_list(); + $this->assertEmpty($reports); + } + + /** + * Test get_base_records() + */ + public function test_get_base_records(): void { + $this->resetAfterTest(); + + // Report with no audiences. + /** @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, + ]); + $baserecords = audience::get_base_records($report->get('id')); + $this->assertEmpty($baserecords); + + // Create a couple of manual audience types. + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $generator->create_audience([ + 'reportid' => $report->get('id'), + 'classname' => manual::class, + 'configdata' => ['users' => [$user1->id, $user2->id]], + ]); + $user3 = $this->getDataGenerator()->create_user(); + $generator->create_audience([ + 'reportid' => $report->get('id'), + 'classname' => manual::class, + 'configdata' => ['users' => [$user3->id]], + ]); + + $baserecords = audience::get_base_records($report->get('id')); + $this->assertCount(2, $baserecords); + $this->assertInstanceOf(manual::class, $baserecords[0]); + $this->assertInstanceOf(manual::class, $baserecords[1]); + } + + /** + * Test get_allowed_reports() + */ + public function test_get_allowed_reports(): void { + $this->resetAfterTest(); + + /** @var core_reportbuilder_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); + + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + self::setUser($user1); + + // No reports. + $reports = audience::get_allowed_reports(); + $this->assertEmpty($reports); + + $report1 = $generator->create_report([ + 'name' => 'My report', + 'source' => users::class, + 'default' => false, + ]); + $report2 = $generator->create_report([ + 'name' => 'My report', + 'source' => users::class, + 'default' => false, + ]); + $report3 = $generator->create_report([ + 'name' => 'My report', + 'source' => users::class, + 'default' => false, + ]); + + // Reports with no audiences set. + $reports = audience::get_allowed_reports(); + $this->assertEmpty($reports); + + $generator->create_audience([ + 'reportid' => $report1->get('id'), + 'classname' => manual::class, + 'configdata' => ['users' => [$user1->id, $user2->id]], + ]); + $generator->create_audience([ + 'reportid' => $report2->get('id'), + 'classname' => manual::class, + 'configdata' => ['users' => [$user2->id]], + ]); + $generator->create_audience([ + 'reportid' => $report3->get('id'), + 'classname' => manual::class, + 'configdata' => ['users' => [$user1->id]], + ]); + + // Purge cache, to ensure allowed reports are re-calculated. + audience::purge_caches(); + + $reports = audience::get_allowed_reports(); + $this->assertEqualsCanonicalizing([$report1->get('id'), $report3->get('id')], $reports); + + // User2 can access report1 and report2. + $reports = audience::get_allowed_reports((int) $user2->id); + $this->assertEqualsCanonicalizing([$report1->get('id'), $report2->get('id')], $reports); + } + + /** + * Test user_reports_list() + */ + public function test_user_reports_list(): void { + $this->resetAfterTest(); + + /** @var core_reportbuilder_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); + + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $user3 = $this->getDataGenerator()->create_user(); + self::setUser($user1); + + $reports = audience::user_reports_list(); + $this->assertEmpty($reports); + + $report1 = $generator->create_report([ + 'name' => 'My report', + 'source' => users::class, + 'default' => false, + ]); + $report2 = $generator->create_report([ + 'name' => 'My report', + 'source' => users::class, + 'default' => false, + ]); + $report3 = $generator->create_report([ + 'name' => 'My report', + 'source' => users::class, + 'default' => false, + ]); + + $generator->create_audience([ + 'reportid' => $report1->get('id'), + 'classname' => manual::class, + 'configdata' => ['users' => [$user1->id, $user2->id]], + ]); + $generator->create_audience([ + 'reportid' => $report2->get('id'), + 'classname' => manual::class, + 'configdata' => ['users' => [$user2->id]], + ]); + $generator->create_audience([ + 'reportid' => $report3->get('id'), + 'classname' => manual::class, + 'configdata' => ['users' => [$user1->id]], + ]); + + // Purge cache, to ensure allowed reports are re-calculated. + audience::purge_caches(); + + // User1 can access report1 and report3. + $reports = audience::user_reports_list(); + $this->assertEqualsCanonicalizing([$report1->get('id'), $report3->get('id')], $reports); + + // User2 can access report1 and report2. + $reports = audience::user_reports_list((int) $user2->id); + $this->assertEqualsCanonicalizing([$report1->get('id'), $report2->get('id')], $reports); + + // User3 can not access any report. + $reports = audience::user_reports_list((int) $user3->id); + $this->assertEmpty($reports); + } + + /** + * Test get_all_audiences_menu_types() + */ + public function test_get_all_audiences_menu_types(): void { + $this->resetAfterTest(); + + // Test with user that has no permission to add audiences. + $user1 = $this->getDataGenerator()->create_user(); + $roleid = create_role('Dummy role', 'dummyrole', 'dummy role description'); + assign_capability('moodle/user:viewalldetails', CAP_PROHIBIT, $roleid, context_system::instance()->id); + role_assign($roleid, $user1->id, context_system::instance()->id); + self::setUser($user1); + $categories = audience::get_all_audiences_menu_types(); + $this->assertEmpty($categories); + + self::setAdminUser(); + $categories = audience::get_all_audiences_menu_types(); + $category = array_filter($categories, function ($category) { + return $category['name'] === 'Site'; + }); + $category = reset($category); + // We don't use assertEqual here to avoid this test failing when more audience types get created. + $this->assertGreaterThanOrEqual(3, $category['items']); + } +} diff --git a/reportbuilder/tests/reportbuilder/audience/allusers_test.php b/reportbuilder/tests/reportbuilder/audience/allusers_test.php new file mode 100644 index 00000000000..f76455c9702 --- /dev/null +++ b/reportbuilder/tests/reportbuilder/audience/allusers_test.php @@ -0,0 +1,152 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\reportbuilder\audience; + +use advanced_testcase; +use context_system; +use core_reportbuilder_generator; +use core_user\reportbuilder\datasource\users; + +/** + * Unit tests for all users report audience type + * + * @package core_reportbuilder + * @covers \core_reportbuilder\reportbuilder\audience\allusers + * @copyright 2021 David Matamoros + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class allusers_test extends advanced_testcase { + + /** + * Test that this audience type description is generated correctly + */ + public function test_get_description(): 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, + 'default' => false, + ]); + + $audience = allusers::create($report->get('id'), []); + $this->assertEquals(get_string('allsiteusers', 'core_reportbuilder'), $audience->get_description()); + } + + /** + * Test if user can add this audience type to the report + */ + public function test_user_can_add(): 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, + 'default' => false, + ]); + + $audience = allusers::create($report->get('id'), []); + + // Admin user. + self::setAdminUser(); + $this->assertTrue($audience->user_can_add()); + + // Non-priveleged user. + $user = self::getDataGenerator()->create_user(); + self::setUser($user); + $this->assertFalse($audience->user_can_add()); + + // Grant priveleges to user. + $roleid = create_role('Dummy role', 'dummyrole', 'dummy role description'); + assign_capability('moodle/user:viewalldetails', CAP_ALLOW, $roleid, context_system::instance()->id); + role_assign($roleid, $user->id, context_system::instance()->id); + $this->assertTrue($audience->user_can_add()); + } + + /** + * Test if user can edit this audience type + */ + public function test_user_can_edit(): 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, + 'default' => false, + ]); + + $audience = allusers::create($report->get('id'), []); + + // Admin user. + self::setAdminUser(); + $this->assertTrue($audience->user_can_edit()); + + // Non-priveleged user. + $user = self::getDataGenerator()->create_user(); + self::setUser($user); + $this->assertFalse($audience->user_can_edit()); + + // Grant priveleges to user. + $roleid = create_role('Dummy role', 'dummyrole', 'dummy role description'); + assign_capability('moodle/user:viewalldetails', CAP_ALLOW, $roleid, context_system::instance()->id); + role_assign($roleid, $user->id, context_system::instance()->id); + $this->assertTrue($audience->user_can_edit()); + } + + /** + * Test that sql generated is correct + */ + public function test_get_sql(): void { + global $DB; + $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, + 'default' => false, + ]); + + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $user3 = $this->getDataGenerator()->create_user(); + $this->getDataGenerator()->create_user(['suspended' => 1]); + $this->getDataGenerator()->create_user(['deleted' => 1]); + + $audience = allusers::create($report->get('id'), []); + + [$join, $where, $params] = $audience->get_sql('u'); + $query = 'SELECT u.* FROM {user} u ' . $join . ' WHERE ' . $where; + $records = $DB->get_records_sql($query, $params); + + $this->assertEqualsCanonicalizing([get_admin()->id, $user1->id, $user2->id, $user3->id], + array_column($records, 'id')); + } +} diff --git a/reportbuilder/tests/reportbuilder/audience/manual_test.php b/reportbuilder/tests/reportbuilder/audience/manual_test.php new file mode 100644 index 00000000000..2462dad5489 --- /dev/null +++ b/reportbuilder/tests/reportbuilder/audience/manual_test.php @@ -0,0 +1,162 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\reportbuilder\audience; + +use advanced_testcase; +use context_system; +use core_reportbuilder_generator; +use core_user\reportbuilder\datasource\users; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Unit tests for manual report audience type + * + * @package core_reportbuilder + * @covers \core_reportbuilder\reportbuilder\audience\manual + * @copyright 2021 David Matamoros + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class manual_test extends advanced_testcase { + + /** + * Test that this audience type description is generated correctly + */ + public function test_get_description(): void { + $this->resetAfterTest(); + + /** @var core_reportbuilder_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); + + $user1 = $this->getDataGenerator()->create_user(['firstname' => 'User', 'lastname' => 'A']); + $user2 = $this->getDataGenerator()->create_user(['firstname' => 'User', 'lastname' => 'B']); + + $report = $generator->create_report([ + 'name' => 'My report', + 'source' => users::class, + 'default' => false, + ]); + + $canviewfullnames = has_capability('moodle/site:viewfullnames', context_system::instance()); + $audience = manual::create($report->get('id'), ['users' => [$user1->id, $user2->id]]); + $this->assertEquals(implode(', ', [fullname($user1, $canviewfullnames), fullname($user2, $canviewfullnames)]), + $audience->get_description()); + } + + /** + * Test if user can add this audience type to the report + */ + public function test_user_can_add(): void { + $this->resetAfterTest(); + + /** @var core_reportbuilder_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); + + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + + $report = $generator->create_report([ + 'name' => 'My report', + 'source' => users::class, + 'default' => false, + ]); + + $audience = manual::create($report->get('id'), ['users' => [$user1->id, $user2->id]]); + + // Admin user. + self::setAdminUser(); + $this->assertTrue($audience->user_can_add()); + + // Non-priveleged user. + $user = self::getDataGenerator()->create_user(); + self::setUser($user); + $this->assertFalse($audience->user_can_add()); + + // Grant priveleges to user. + $roleid = create_role('Dummy role', 'dummyrole', 'dummy role description'); + assign_capability('moodle/user:viewalldetails', CAP_ALLOW, $roleid, context_system::instance()->id); + role_assign($roleid, $user->id, context_system::instance()->id); + $this->assertTrue($audience->user_can_add()); + } + + /** + * Test if user can edit this audience type + */ + public function test_user_can_edit(): void { + $this->resetAfterTest(); + + /** @var core_reportbuilder_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); + + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + + $report = $generator->create_report([ + 'name' => 'My report', + 'source' => users::class, + 'default' => false, + ]); + + $audience = manual::create($report->get('id'), ['users' => [$user1->id, $user2->id]]); + + // Admin user. + self::setAdminUser(); + $this->assertTrue($audience->user_can_edit()); + + // Non-priveleged user. + $user = self::getDataGenerator()->create_user(); + self::setUser($user); + $this->assertFalse($audience->user_can_edit()); + + // Grant priveleges to user. + $roleid = create_role('Dummy role', 'dummyrole', 'dummy role description'); + assign_capability('moodle/user:viewalldetails', CAP_ALLOW, $roleid, context_system::instance()->id); + role_assign($roleid, $user->id, context_system::instance()->id); + $this->assertTrue($audience->user_can_edit()); + } + + /** + * Test that sql generated is correct + */ + public function test_get_sql(): void { + global $DB; + $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, + 'default' => false, + ]); + + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $user3 = $this->getDataGenerator()->create_user(); + + $audience = manual::create($report->get('id'), ['users' => [$user1->id, $user3->id]]); + + [$join, $where, $params] = $audience->get_sql('u'); + $query = 'SELECT u.* FROM {user} u ' . $join . ' WHERE ' . $where; + $records = $DB->get_records_sql($query, $params); + + $this->assertEqualsCanonicalizing([$user1->id, $user3->id], array_column($records, 'id')); + } +} diff --git a/reportbuilder/tests/reportbuilder/audience/systemrole_test.php b/reportbuilder/tests/reportbuilder/audience/systemrole_test.php new file mode 100644 index 00000000000..5f97942c8ad --- /dev/null +++ b/reportbuilder/tests/reportbuilder/audience/systemrole_test.php @@ -0,0 +1,162 @@ +. + +declare(strict_types=1); + +namespace core_reportbuilder\reportbuilder\audience; + +use advanced_testcase; +use context_system; +use core_reportbuilder_generator; +use core_user\reportbuilder\datasource\users; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Unit tests for system role report audience type + * + * @package core_reportbuilder + * @covers \core_reportbuilder\reportbuilder\audience\systemrole + * @copyright 2021 David Matamoros + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class systemrole_test extends advanced_testcase { + + /** + * Test that this audience type description is generated correctly + */ + public function test_get_description(): void { + global $DB; + + $this->resetAfterTest(); + + /** @var core_reportbuilder_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); + + self::setAdminUser(); + + $role = $DB->get_record('role', ['shortname' => 'manager']); + $rolename = role_get_name($role, null, ROLENAME_ALIAS); + + $report = $generator->create_report([ + 'name' => 'My report', + 'source' => users::class, + 'default' => false, + ]); + + $audience = systemrole::create($report->get('id'), ['roles' => [$role->id]]); + $this->assertEquals($rolename, $audience->get_description()); + } + + /** + * Test if user can add this audience type to the report + */ + public function test_user_can_add(): void { + $this->resetAfterTest(); + + /** @var core_reportbuilder_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); + + self::setAdminUser(); + $roles = get_assignable_roles(context_system::instance(), ROLENAME_ALIAS); + + $report = $generator->create_report([ + 'name' => 'My report', + 'source' => users::class, + 'default' => false, + ]); + + $audience = systemrole::create($report->get('id'), ['roles' => array_keys($roles)]); + + // Admin user. + self::setAdminUser(); + $this->assertTrue($audience->user_can_add()); + + // Non-priveleged user. + $user = self::getDataGenerator()->create_user(); + self::setUser($user); + $this->assertFalse($audience->user_can_add()); + } + + /** + * Test if user can edit this audience type + */ + public function test_user_can_edit(): void { + $this->resetAfterTest(); + + /** @var core_reportbuilder_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); + + self::setAdminUser(); + $roles = get_assignable_roles(context_system::instance(), ROLENAME_ALIAS); + $this->assertNotEmpty($roles); + + $report = $generator->create_report([ + 'name' => 'My report', + 'source' => users::class, + 'default' => false, + ]); + + $audience = systemrole::create($report->get('id'), ['roles' => array_keys($roles)]); + + // Admin user. + self::setAdminUser(); + $this->assertTrue($audience->user_can_edit()); + + // Non-priveleged user. + $user = self::getDataGenerator()->create_user(); + self::setUser($user); + $this->assertFalse($audience->user_can_edit()); + } + + /** + * Test that sql generated is correct + */ + public function test_get_sql(): void { + global $DB; + $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, + 'default' => false, + ]); + + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $user3 = $this->getDataGenerator()->create_user(); + + self::setAdminUser(); + $roles = get_assignable_roles(context_system::instance(), ROLENAME_ALIAS); + $this->assertNotEmpty($roles); + + // Assign new role to user1 and user3. + $roleid = create_role('Dummy role', 'dummyrole', 'dummy role description'); + role_assign($roleid, $user1->id, context_system::instance()->id); + role_assign($roleid, $user3->id, context_system::instance()->id); + + $audience = systemrole::create($report->get('id'), ['roles' => [$roleid]]); + + [$join, $where, $params] = $audience->get_sql('u'); + $query = 'SELECT u.* FROM {user} u ' . $join . ' WHERE ' . $where; + $records = $DB->get_records_sql($query, $params); + + $this->assertEqualsCanonicalizing([$user1->id, $user3->id], array_column($records, 'id')); + } +}