MDL-83345 reportbuilder: migrate filter storage from user preferences.

Use a dedicated persistent table/model for storage of a users' report
filter configuration.
This commit is contained in:
Paul Holden 2024-10-22 13:08:57 +01:00
parent a798b1db39
commit f7e0e8cd96
No known key found for this signature in database
GPG Key ID: A81A96D6045F6164
11 changed files with 330 additions and 244 deletions

View File

@ -135,6 +135,7 @@ filterbothactive,core_grades
filterbyname,core_grades
filterfirstactive,core_grades
filterlastactive,core_grades
privacy:metadata:preference:reportfilter,core_reportbuilder
noreplybouncemessage,core
noreplybouncesubject,core
configgeneralquestionbank,core_backup

View File

@ -200,7 +200,6 @@ $string['privacy:metadata:filter'] = 'Report filter definitions';
$string['privacy:metadata:filter:uniqueidentifier'] = 'Unique identifier of the filter';
$string['privacy:metadata:filter:usercreated'] = 'The ID of the user who created the filter';
$string['privacy:metadata:filter:usermodified'] = 'The ID of the user who last modified the filter';
$string['privacy:metadata:preference:reportfilter'] = 'Stored report filter values';
$string['privacy:metadata:report'] = 'Report definitions';
$string['privacy:metadata:report:conditiondata'] = 'Configuration data for the report conditions';
$string['privacy:metadata:report:name'] = 'The name of the report';
@ -226,6 +225,10 @@ $string['privacy:metadata:schedule:timescheduled'] = 'The time the schedule will
$string['privacy:metadata:schedule:usercreated'] = 'The ID of the user who created the schedule';
$string['privacy:metadata:schedule:usermodified'] = 'The ID of the user who last modified the schedule';
$string['privacy:metadata:schedule:userviewas'] = 'The ID of the user who the schedule will be viewed as';
$string['privacy:metadata:user_filter'] = 'Report user filter definitions';
$string['privacy:metadata:user_filter:filterdata'] = 'Configuration data for the report filters';
$string['privacy:metadata:user_filter:timecreated'] = 'The time when the user filter was created';
$string['privacy:metadata:user_filter:timemodified'] = 'The time when the user filter was last modified';
$string['recurrence'] = 'Recurrence';
$string['recurrenceannually'] = 'Annually';
$string['recurrencedaily'] = 'Daily';
@ -295,3 +298,6 @@ $string['filterdurationunit'] = '{$a} unit';
// Deprecated since Moodle 4.5.
$string['filterdatefrom'] = 'Date from';
$string['filterdateto'] = 'Date to';
// Deprecated since Moodle 5.0.
$string['privacy:metadata:preference:reportfilter'] = 'Stored report filter values';

View File

@ -4621,6 +4621,26 @@
<INDEX NAME="report-filter" UNIQUE="true" FIELDS="reportid, uniqueidentifier, iscondition"/>
</INDEXES>
</TABLE>
<TABLE NAME="reportbuilder_user_filter" COMMENT="Defines user report filter">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="reportid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="filterdata" TYPE="text" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="usercreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="usermodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="reportid" TYPE="foreign" FIELDS="reportid" REFTABLE="reportbuilder_report" REFFIELDS="id"/>
<KEY NAME="usercreated" TYPE="foreign" FIELDS="usercreated" REFTABLE="user" REFFIELDS="id"/>
<KEY NAME="usermodified" TYPE="foreign" FIELDS="usermodified" REFTABLE="user" REFFIELDS="id"/>
</KEYS>
<INDEXES>
<INDEX NAME="report-user" UNIQUE="true" FIELDS="reportid, usercreated"/>
</INDEXES>
</TABLE>
<TABLE NAME="reportbuilder_audience" COMMENT="Defines report audience">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>

View File

@ -1236,5 +1236,37 @@ function xmldb_main_upgrade($oldversion) {
upgrade_main_savepoint(true, 2024111500.04);
}
if ($oldversion < 2024112900.01) {
// Define table reportbuilder_user_filter to be created.
$table = new xmldb_table('reportbuilder_user_filter');
// Adding fields to table reportbuilder_user_filter.
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
$table->add_field('reportid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
$table->add_field('filterdata', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null);
$table->add_field('usercreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
$table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
$table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
$table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
// Adding keys to table reportbuilder_user_filter.
$table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
$table->add_key('reportid', XMLDB_KEY_FOREIGN, ['reportid'], 'reportbuilder_report', ['id']);
$table->add_key('usercreated', XMLDB_KEY_FOREIGN, ['usercreated'], 'user', ['id']);
$table->add_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
// Adding indexes to table reportbuilder_user_filter.
$table->add_index('report-user', XMLDB_INDEX_UNIQUE, ['reportid', 'usercreated']);
// Conditionally launch create table for reportbuilder_user_filter.
if (!$dbman->table_exists($table)) {
$dbman->create_table($table);
}
// Main savepoint reached.
upgrade_main_savepoint(true, 2024112900.01);
}
return true;
}

View File

@ -18,36 +18,18 @@ declare(strict_types=1);
namespace core_reportbuilder\local\helpers;
use core_reportbuilder\local\models\user_filter;
use core_text;
/**
* This class handles the setting and retrieving of a users' filter values for given reports
*
* It is currently using the user preference API as a storage mechanism
*
* @package core_reportbuilder
* @copyright 2021 Paul Holden <paulh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class user_filter_manager {
/** @var int The size of each chunk, matching the maximum length of a single user preference */
private const PREFERENCE_CHUNK_SIZE = 1333;
/** @var string The prefix used to name the stored user preferences */
private const PREFERENCE_NAME_PREFIX = 'reportbuilder-report-';
/**
* Generate user preference name for given report
*
* @param int $reportid
* @param int $index
* @return string
*/
private static function user_preference_name(int $reportid, int $index): string {
return static::PREFERENCE_NAME_PREFIX . "{$reportid}-{$index}";
}
/**
* Set user filters for given report
*
@ -57,16 +39,20 @@ class user_filter_manager {
* @return bool
*/
public static function set(int $reportid, array $values, ?int $userid = null): bool {
$jsonvalues = json_encode($values);
global $USER;
$jsonchunks = str_split($jsonvalues, static::PREFERENCE_CHUNK_SIZE);
foreach ($jsonchunks as $index => $jsonchunk) {
$userpreference = static::user_preference_name($reportid, $index);
set_user_preference($userpreference, $jsonchunk, $userid);
$userid ??= $USER->id;
$userfilter = user_filter::get_record(['reportid' => $reportid, 'usercreated' => $userid]);
if ($userfilter === false) {
$userfilter = new user_filter(0, (object) [
'reportid' => $reportid,
'usercreated' => $userid,
]);
}
// Ensure any subsequent preferences are reset (to account for number of chunks decreasing).
static::reset_all($reportid, $userid, $index + 1);
$userfilter->set('filterdata', json_encode($values))
->save();
return true;
}
@ -79,17 +65,16 @@ class user_filter_manager {
* @return array
*/
public static function get(int $reportid, ?int $userid = null): array {
$jsonvalues = '';
$index = 0;
global $USER;
// We'll repeatedly append chunks to our JSON string, until we hit one that is below the maximum length.
do {
$userpreference = static::user_preference_name($reportid, $index++);
$jsonchunk = get_user_preferences($userpreference, '', $userid);
$jsonvalues .= $jsonchunk;
} while (core_text::strlen($jsonchunk) === static::PREFERENCE_CHUNK_SIZE);
$userid ??= $USER->id;
return (array) json_decode($jsonvalues);
$userfilter = user_filter::get_record(['reportid' => $reportid, 'usercreated' => $userid]);
if ($userfilter === false) {
return [];
}
return (array) json_decode($userfilter->get('filterdata'));
}
/**
@ -111,18 +96,15 @@ class user_filter_manager {
*
* @param int $reportid
* @param int|null $userid
* @param int $index If specified, then preferences will be reset starting from this index
* @param int $index Unused
* @return bool
*/
public static function reset_all(int $reportid, ?int $userid = null, int $index = 0): bool {
// We'll repeatedly retrieve and reset preferences, until we hit one that is below the maximum length.
do {
$userpreference = static::user_preference_name($reportid, $index++);
$jsonchunk = get_user_preferences($userpreference, '', $userid);
unset_user_preference($userpreference, $userid);
} while (core_text::strlen($jsonchunk) === static::PREFERENCE_CHUNK_SIZE);
global $DB, $USER;
return true;
$userid ??= $USER->id;
return $DB->delete_records(user_filter::TABLE, ['reportid' => $reportid, 'usercreated' => $userid]);
}
/**
@ -147,27 +129,21 @@ class user_filter_manager {
/**
* Get all report filters for given user
*
* This is primarily designed for the privacy provider, and allows us to preserve all the preference logic within this class.
*
* @param int $userid
* @return array
*
* @deprecated since Moodle 5.0 - please do not use this function any more
*/
#[\core\attribute\deprecated(null, reason: 'It is no longer used', mdl: 'MDL-83345', since: '5.0')]
public static function get_all_for_user(int $userid): array {
global $DB;
\core\deprecation::emit_deprecation_if_present([self::class, __FUNCTION__]);
$prefs = [];
// We need to locate the first preference chunk of all report filters.
$select = 'userid = :userid AND ' . $DB->sql_like('name', ':namelike');
$params = [
'userid' => $userid,
'namelike' => $DB->sql_like_escape(static::PREFERENCE_NAME_PREFIX) . '%-0',
];
$preferences = $DB->get_fieldset_select('user_preferences', 'name', $select, $params);
// Retrieve all found filters.
$preferences = user_filter::get_records(['usercreated' => $userid]);
foreach ($preferences as $preference) {
preg_match('/^' . static::PREFERENCE_NAME_PREFIX . '(?<reportid>\d+)\-/', $preference, $matches);
$prefs[static::PREFERENCE_NAME_PREFIX . $matches['reportid']] = static::get((int) $matches['reportid'], $userid);
$prefs['reportbuilder-report-' . $preference->get('reportid')] = (array) json_decode($preference->get('filterdata'));
}
return $prefs;

View File

@ -128,6 +128,11 @@ class report extends persistent {
$filter->delete();
}
// User filters.
foreach (user_filter::get_records($reportparams) as $userfilter) {
$userfilter->delete();
}
// Audiences.
foreach (audience::get_records($reportparams) as $audience) {
$audience->delete();

View File

@ -0,0 +1,73 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core_reportbuilder\local\models;
use lang_string;
use core\persistent;
/**
* Persistent class to represent a user report filter
*
* @package core_reportbuilder
* @copyright 2024 Paul Holden <paulh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class user_filter extends persistent {
/** @var string The table name. */
public const TABLE = 'reportbuilder_user_filter';
/**
* Return the definition of the properties of this model.
*
* @return array
*/
protected static function define_properties(): array {
return [
'reportid' => [
'type' => PARAM_INT,
],
'filterdata' => [
'type' => PARAM_RAW,
],
'usercreated' => [
'type' => PARAM_INT,
'default' => static function(): int {
global $USER;
return (int) $USER->id;
},
],
];
}
/**
* Validate reportid property
*
* @param int $reportid
* @return bool|lang_string
*/
protected function validate_reportid(int $reportid) {
if (!report::record_exists($reportid)) {
return new lang_string('invaliddata', 'error');
}
return true;
}
}

View File

@ -27,16 +27,10 @@ use core_privacy\local\request\contextlist;
use core_privacy\local\request\core_userlist_provider;
use core_privacy\local\request\transform;
use core_privacy\local\request\userlist;
use core_privacy\local\request\user_preference_provider;
use core_privacy\local\request\writer;
use core_reportbuilder\manager;
use core_reportbuilder\local\helpers\user_filter_manager;
use core_reportbuilder\local\helpers\schedule as schedule_helper;
use core_reportbuilder\local\models\audience;
use core_reportbuilder\local\models\column;
use core_reportbuilder\local\models\filter;
use core_reportbuilder\local\models\report;
use core_reportbuilder\local\models\schedule;
use core_reportbuilder\local\models\{audience, column, filter, report, schedule, user_filter};
/**
* Privacy Subsystem for core_reportbuilder
@ -48,8 +42,7 @@ use core_reportbuilder\local\models\schedule;
class provider implements
\core_privacy\local\metadata\provider,
\core_privacy\local\request\plugin\provider,
core_userlist_provider,
user_preference_provider {
core_userlist_provider {
/**
* Returns metadata about the component
@ -82,6 +75,12 @@ class provider implements
'usermodified' => 'privacy:metadata:filter:usermodified',
], 'privacy:metadata:filter');
$collection->add_database_table(user_filter::TABLE, [
'filterdata' => 'privacy:metadata:user_filter:filterdata',
'timecreated' => 'privacy:metadata:user_filter:timecreated',
'timemodified' => 'privacy:metadata:user_filter:timemodified',
], 'privacy:metadata:user_filter');
$collection->add_database_table(audience::TABLE, [
'classname' => 'privacy:metadata:audience:classname',
'configdata' => 'privacy:metadata:audience:configdata',
@ -109,29 +108,9 @@ class provider implements
'timemodified' => 'privacy:metadata:schedule:timemodified',
], 'privacy:metadata:schedule');
$collection->add_user_preference('core_reportbuilder', 'privacy:metadata:preference:reportfilter');
return $collection;
}
/**
* Export all user preferences for the component
*
* @param int $userid
*/
public static function export_user_preferences(int $userid): void {
$preferencestring = get_string('privacy:metadata:preference:reportfilter', 'core_reportbuilder');
$filters = user_filter_manager::get_all_for_user($userid);
foreach ($filters as $key => $filter) {
writer::export_user_preference('core_reportbuilder',
$key,
json_encode($filter, JSON_PRETTY_PRINT),
$preferencestring
);
}
}
/**
* Get export sub context for a report
*
@ -156,7 +135,7 @@ class provider implements
public static function get_contexts_for_userid(int $userid): contextlist {
$contextlist = new contextlist();
// Locate all contexts for reports the user has created, or reports they have created audience/schedules for.
// Locate all contexts for reports the user has created, or reports they have created ancillary data for.
$sql = '
SELECT r.contextid
FROM {' . report::TABLE . '} r
@ -164,6 +143,10 @@ class provider implements
AND (r.usercreated = ?
OR r.usermodified = ?
OR r.id IN (
SELECT f.reportid
FROM {' . user_filter::TABLE . '} f
WHERE f.usercreated = ?
UNION
SELECT a.reportid
FROM {' . audience::TABLE . '} a
WHERE a.usercreated = ? OR a.usermodified = ?
@ -174,7 +157,7 @@ class provider implements
)
)';
return $contextlist->add_from_sql($sql, array_fill(0, 6, $userid));
return $contextlist->add_from_sql($sql, array_fill(0, 7, $userid));
}
/**
@ -193,6 +176,13 @@ class provider implements
$userlist->add_from_sql('usercreated', $sql, $params);
$userlist->add_from_sql('usermodified', $sql, $params);
// Users who have created user filters.
$sql = 'SELECT f.usercreated
FROM {' . user_filter::TABLE . '} f
JOIN {' . report::TABLE . '} r ON r.id = f.reportid
WHERE ' . $select;
$userlist->add_from_sql('usercreated', $sql, $params);
// Users who have created audiences.
$sql = 'SELECT a.usercreated, a.usermodified
FROM {' . audience::TABLE . '} a
@ -224,6 +214,10 @@ class provider implements
// We need to get all reports that the user has created, or reports they have created audience/schedules for.
$select = 'type = 0 AND (usercreated = ? OR usermodified = ? OR id IN (
SELECT f.reportid
FROM {' . user_filter::TABLE . '} f
WHERE f.usercreated = ?
UNION
SELECT a.reportid
FROM {' . audience::TABLE . '} a
WHERE a.usercreated = ? OR a.usermodified = ?
@ -232,13 +226,18 @@ class provider implements
FROM {' . schedule::TABLE . '} s
WHERE s.usercreated = ? OR s.usermodified = ?
))';
$params = array_fill(0, 6, $user->id);
$params = array_fill(0, 7, $user->id);
foreach (report::get_records_select($select, $params) as $report) {
$subcontext = static::get_export_subcontext($report);
self::export_report($subcontext, $report);
// User filters.
if ($userfilters = user_filter::get_records(['reportid' => $report->get('id'), 'usercreated' => $user->id])) {
static::export_user_filters($report->get_context(), $subcontext, $userfilters);
}
$select = 'reportid = ? AND (usercreated = ? OR usermodified = ?)';
$params = [$report->get('id'), $user->id, $user->id];
@ -309,6 +308,25 @@ class provider implements
writer::with_context($report->get_context())->export_data($subcontext, $reportdata);
}
/**
* Export given user filters in context
*
* @param context $context
* @param array $subcontext
* @param user_filter[] $userfilters
*/
protected static function export_user_filters(context $context, array $subcontext, array $userfilters): void {
$userfilterdata = array_map(static function(user_filter $userfilter): stdClass {
return (object) [
'filterdata' => $userfilter->get('filterdata'),
'timecreated' => transform::datetime($userfilter->get('timecreated')),
'timemodified' => transform::datetime($userfilter->get('timemodified')),
];
}, $userfilters);
writer::with_context($context)->export_related_data($subcontext, 'userfilters', (object) ['data' => $userfilterdata]);
}
/**
* Export given audiences in context
*

View File

@ -19,6 +19,9 @@ declare(strict_types=1);
namespace core_reportbuilder\local\helpers;
use advanced_testcase;
use core_reportbuilder_generator;
use core_reportbuilder\local\models\user_filter;
use core_user\reportbuilder\datasource\users;
/**
* Unit tests for the user filter helper
@ -28,18 +31,7 @@ use advanced_testcase;
* @copyright 2021 Paul Holden <paulh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class user_filter_manager_test extends advanced_testcase {
/**
* Helper method to return all user preferences for filters - based on the current storage backend using the same
*
* @return array
*/
private function get_filter_preferences(): array {
return array_filter(get_user_preferences(), static function(string $key): bool {
return strpos($key, 'reportbuilder-report-') === 0;
}, ARRAY_FILTER_USE_KEY);
}
final class user_filter_manager_test extends advanced_testcase {
/**
* Data provider for {@see test_get}
@ -64,48 +56,30 @@ class user_filter_manager_test extends advanced_testcase {
public function test_get(string $value): 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]);
$values = [
'entity:filter_name' => $value,
];
user_filter_manager::set(5, $values);
user_filter_manager::set($report->get('id'), $values);
// Make sure we get the same value back.
$this->assertEquals($values, user_filter_manager::get(5));
}
/**
* Test getting filter values that once spanned multiple chunks
*/
public function test_get_large_to_small(): void {
$this->resetAfterTest();
// Set a large initial filter value.
user_filter_manager::set(5, [
'longvalue' => str_repeat('ABCD', 1000),
]);
// Sanity check, there should be 4 (because 4000 characters plus some JSON encoding requires that many chunks).
$preferences = $this->get_filter_preferences();
$this->assertCount(4, $preferences);
$values = [
'longvalue' => 'ABCD',
];
user_filter_manager::set(5, $values);
// Make sure we get the same value back.
$this->assertEquals($values, user_filter_manager::get(5));
// Everything should now fit in a single filter preference.
$preferences = $this->get_filter_preferences();
$this->assertCount(1, $preferences);
$this->assertEquals($values, user_filter_manager::get($report->get('id')));
}
/**
* Test getting filter values that haven't been set
*/
public function test_get_empty(): void {
$this->assertEquals([], user_filter_manager::get(5));
$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]);
$this->assertEquals([], user_filter_manager::get($report->get('id')));
}
/**
@ -131,18 +105,22 @@ class user_filter_manager_test extends advanced_testcase {
public function test_reset_all(string $value): void {
$this->resetAfterTest();
user_filter_manager::set(5, [
'entity:filter_name' => $value
/** @var core_reportbuilder_generator $generator */
$generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder');
$report = $generator->create_report(['name' => 'My report', 'source' => users::class]);
user_filter_manager::set($report->get('id'), [
'entity:filter_name' => $value,
]);
$reset = user_filter_manager::reset_all(5);
$reset = user_filter_manager::reset_all($report->get('id'));
$this->assertTrue($reset);
// We should get an empty array back.
$this->assertEquals([], user_filter_manager::get(5));
$this->assertEquals([], user_filter_manager::get($report->get('id')));
// All filter preferences should be removed.
$this->assertEmpty($this->get_filter_preferences());
$this->assertFalse(user_filter::get_record(['reportid' => $report->get('id')]));
}
/**
@ -151,20 +129,24 @@ class user_filter_manager_test extends advanced_testcase {
public function test_reset_single(): void {
$this->resetAfterTest();
user_filter_manager::set(5, [
/** @var core_reportbuilder_generator $generator */
$generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder');
$report = $generator->create_report(['name' => 'My report', 'source' => users::class]);
user_filter_manager::set($report->get('id'), [
'entity:filter_name' => 'foo',
'entity:filter_value' => 'bar',
'entity:other_name' => 'baz',
'entity:other_value' => 'bax',
]);
$reset = user_filter_manager::reset_single(5, 'entity:other');
$reset = user_filter_manager::reset_single($report->get('id'), 'entity:other');
$this->assertTrue($reset);
$this->assertEquals([
'entity:filter_name' => 'foo',
'entity:filter_value' => 'bar',
], user_filter_manager::get(5));
], user_filter_manager::get($report->get('id')));
}
/**
@ -173,30 +155,29 @@ class user_filter_manager_test extends advanced_testcase {
public function test_merge(): void {
$this->resetAfterTest();
$values = [
/** @var core_reportbuilder_generator $generator */
$generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder');
$report = $generator->create_report(['name' => 'My report', 'source' => users::class]);
user_filter_manager::set($report->get('id'), [
'entity:filter_name' => 'foo',
'entity:filter_value' => 'bar',
'entity:filter2_name' => 'tree',
'entity:filter2_value' => 'house',
];
]);
// Make sure we get the same value back.
user_filter_manager::set(5, $values);
$this->assertEqualsCanonicalizing($values, user_filter_manager::get(5));
user_filter_manager::merge(5, [
// Make sure that both values have been changed and the other values have not been modified.
user_filter_manager::merge($report->get('id'), [
'entity:filter_name' => 'twotimesfoo',
'entity:filter_value' => 'twotimesbar',
]);
// Make sure that both values have been changed and the other values have not been modified.
$expected = [
$this->assertEqualsCanonicalizing([
'entity:filter_name' => 'twotimesfoo',
'entity:filter_value' => 'twotimesbar',
'entity:filter2_name' => 'tree',
'entity:filter2_value' => 'house',
];
$this->assertEqualsCanonicalizing($expected, user_filter_manager::get(5));
], user_filter_manager::get($report->get('id')));
}
/**
@ -204,30 +185,37 @@ class user_filter_manager_test extends advanced_testcase {
*/
public function test_get_all_for_user(): void {
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
$filtervalues1 = [
/** @var core_reportbuilder_generator $generator */
$generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder');
$reportone = $generator->create_report(['name' => 'Report 1', 'source' => users::class]);
$reporttwo = $generator->create_report(['name' => 'Report 2', 'source' => users::class]);
$userone = $this->getDataGenerator()->create_user();
$usertwo = $this->getDataGenerator()->create_user();
$reportonefilter = [
'entity:filter_name' => 'foo',
'entity:filter_value' => 'bar',
'entity:other_name' => 'baz',
'entity:other_value' => 'bax',
];
user_filter_manager::set(5, $filtervalues1);
user_filter_manager::set($reportone->get('id'), $reportonefilter, (int) $userone->id);
$filtervalues2 = [
$reporttwofilter = [
'entity:filter_name' => 'blue',
'entity:filter_value' => 'red',
];
user_filter_manager::set(9, $filtervalues2);
user_filter_manager::set($reporttwo->get('id'), $reporttwofilter, (int) $userone->id);
$this->setAdminUser();
$values = user_filter_manager::get_all_for_user((int)$user->id);
$this->assertEqualsCanonicalizing([$filtervalues1, $filtervalues2], [reset($values), end($values)]);
// First user has filters in two reports.
$useronefilters = user_filter_manager::get_all_for_user((int) $userone->id);
$this->assertDebuggingCalled();
$this->assertEqualsCanonicalizing([$reportonefilter, $reporttwofilter], $useronefilters);
// Check for a user with no filters.
$user2 = $this->getDataGenerator()->create_user();
$values = user_filter_manager::get_all_for_user((int)$user2->id);
$this->assertEmpty($values);
$usertwofilters = user_filter_manager::get_all_for_user((int) $usertwo->id);
$this->assertDebuggingCalled();
$this->assertEmpty($usertwofilters);
}
}

View File

@ -21,18 +21,12 @@ namespace core_reportbuilder\privacy;
use context_system;
use core_privacy\local\metadata\collection;
use core_privacy\local\metadata\types\database_table;
use core_privacy\local\metadata\types\user_preference;
use core_privacy\local\request\userlist;
use core_privacy\local\request\writer;
use core_privacy\tests\provider_testcase;
use core_reportbuilder_generator;
use core_reportbuilder\manager;
use core_reportbuilder\local\helpers\user_filter_manager;
use core_reportbuilder\local\models\audience;
use core_reportbuilder\local\models\column;
use core_reportbuilder\local\models\filter;
use core_reportbuilder\local\models\report;
use core_reportbuilder\local\models\schedule;
use core_reportbuilder\local\models\{audience, column, filter, report, schedule, user_filter};
use core_user\reportbuilder\datasource\users;
/**
@ -43,7 +37,7 @@ use core_user\reportbuilder\datasource\users;
* @copyright 2021 David Matamoros <davidmc@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider_test extends provider_testcase {
final class provider_test extends provider_testcase {
/**
* Test provider metadata
@ -53,23 +47,14 @@ class provider_test extends provider_testcase {
$metadata = provider::get_metadata($collection)->get_collection();
$this->assertCount(6, $metadata);
$this->assertContainsOnlyInstancesOf(database_table::class, $metadata);
$this->assertInstanceOf(database_table::class, $metadata[0]);
$this->assertEquals(report::TABLE, $metadata[0]->get_name());
$this->assertInstanceOf(database_table::class, $metadata[1]);
$this->assertEquals(column::TABLE, $metadata[1]->get_name());
$this->assertInstanceOf(database_table::class, $metadata[2]);
$this->assertEquals(filter::TABLE, $metadata[2]->get_name());
$this->assertInstanceOf(database_table::class, $metadata[3]);
$this->assertEquals(audience::TABLE, $metadata[3]->get_name());
$this->assertInstanceOf(database_table::class, $metadata[4]);
$this->assertEquals(schedule::TABLE, $metadata[4]->get_name());
$this->assertInstanceOf(user_preference::class, $metadata[5]);
$this->assertEquals(user_filter::TABLE, $metadata[3]->get_name());
$this->assertEquals(audience::TABLE, $metadata[4]->get_name());
$this->assertEquals(schedule::TABLE, $metadata[5]->get_name());
}
/**
@ -90,6 +75,28 @@ class provider_test extends provider_testcase {
$this->assertInstanceOf(context_system::class, $contextlist->current());
}
/**
* Test getting contexts for user who created a user filter for a report by another user
*/
public function test_get_contexts_for_userid_user_filter(): void {
$this->resetAfterTest();
$this->setAdminUser();
/** @var core_reportbuilder_generator $generator */
$generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder');
$report = $generator->create_report(['name' => 'Users', 'source' => users::class]);
// Switch user, create a report audience.
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
user_filter_manager::set($report->get('id'), ['entity:filter_name' => 1]);
$contextlist = $this->get_contexts_for_userid((int) $user->id, 'core_reportbuilder');
$this->assertCount(1, $contextlist);
$this->assertInstanceOf(context_system::class, $contextlist->current());
}
/**
* Test getting contexts for user who created an audience for a report by another user
*/
@ -149,6 +156,12 @@ class provider_test extends provider_testcase {
$report = $generator->create_report(['name' => 'Users', 'source' => users::class]);
// Switch user, create a user filter.
$filteruser = $this->getDataGenerator()->create_user();
$this->setUser($filteruser);
user_filter_manager::set($report->get('id'), ['entity:filter_name' => 1]);
// Switch user, create a report audience.
$audienceuser = $this->getDataGenerator()->create_user();
$this->setUser($audienceuser);
@ -166,6 +179,7 @@ class provider_test extends provider_testcase {
$this->assertEqualsCanonicalizing([
$reportuser->id,
$filteruser->id,
$audienceuser->id,
$scheduleuser->id,
], $userlist->get_userids());
@ -192,6 +206,8 @@ class provider_test extends provider_testcase {
'name' => 'My schedule',
]);
user_filter_manager::set($report->get('id'), ['entity:filter_name' => 1]);
$context = context_system::instance();
$this->export_context_data_for_user((int) $user->id, $context, 'core_reportbuilder');
@ -210,6 +226,16 @@ class provider_test extends provider_testcase {
$this->assertNotEmpty($reportdata->timecreated);
$this->assertNotEmpty($reportdata->timemodified);
// Exported user filter data.
$userfilterdata = $writer->get_related_data($subcontext, 'userfilters')->data;
$this->assertCount(1, $userfilterdata);
$userfilterdata = reset($userfilterdata);
$this->assertEquals('{"entity:filter_name":1}', $userfilterdata->filterdata);
$this->assertNotEmpty($userfilterdata->timecreated);
$this->assertNotEmpty($userfilterdata->timemodified);
// Exported audience data.
$audiencedata = $writer->get_related_data($subcontext, 'audiences')->data;
@ -262,63 +288,4 @@ class provider_test extends provider_testcase {
$this->assertFalse(writer::with_context($context)->has_any_data());
}
/**
* Test to check export_user_preferences.
*/
public function test_export_user_preferences(): void {
$this->resetAfterTest();
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
$this->setUser($user1);
// Create report and set some filters for the user.
$report1 = manager::create_report_persistent((object) [
'type' => 1,
'source' => 'class',
]);
$filtervalues1 = [
'task_log:name_operator' => 0,
'task_log:name_value' => 'My task logs',
];
user_filter_manager::set($report1->get('id'), $filtervalues1);
// Add a filter for user2.
$filtervalues1user2 = [
'task_log:name_operator' => 0,
'task_log:name_value' => 'My task logs user2',
];
user_filter_manager::set($report1->get('id'), $filtervalues1user2, (int)$user2->id);
// Create a second report and set some filters for the user.
$report2 = manager::create_report_persistent((object) [
'type' => 1,
'source' => 'class',
]);
$filtervalues2 = [
'config_change:setting_operator' => 0,
'config_change:setting_value' => str_repeat('A', 3000),
];
user_filter_manager::set($report2->get('id'), $filtervalues2);
// Switch to admin user (so we can validate preferences of our test user are still exported).
$this->setAdminUser();
// Export user preferences.
provider::export_user_preferences((int)$user1->id);
$writer = writer::with_context(context_system::instance());
$prefs = $writer->get_user_preferences('core_reportbuilder');
// Check that user preferences only contain the 2 preferences from user1.
$this->assertCount(2, (array)$prefs);
// Check that exported user preferences for report1 are correct.
$report1key = 'reportbuilder-report-' . $report1->get('id');
$this->assertEquals(json_encode($filtervalues1, JSON_PRETTY_PRINT), $prefs->$report1key->value);
// Check that exported user preferences for report2 are correct.
$report2key = 'reportbuilder-report-' . $report2->get('id');
$this->assertEquals(json_encode($filtervalues2, JSON_PRETTY_PRINT), $prefs->$report2key->value);
}
}

View File

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