Merge branch 'MDL-63700-master' of git://github.com/mickhawkins/moodle

This commit is contained in:
David Monllao 2018-11-05 11:13:51 +01:00
commit d9c54de37f
2 changed files with 314 additions and 11 deletions

View File

@ -28,8 +28,10 @@ use core_privacy\local\request\transform;
use core_privacy\local\request\writer;
use core_privacy\local\metadata\collection;
use core_privacy\local\request\approved_contextlist;
use core_privacy\local\request\approved_userlist;
use core_privacy\local\request\context;
use core_privacy\local\request\contextlist;
use core_privacy\local\request\userlist;
defined('MOODLE_INTERNAL') || die();
@ -39,7 +41,10 @@ defined('MOODLE_INTERNAL') || die();
* @copyright 2018 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\provider {
class provider implements
\core_privacy\local\metadata\provider,
\core_privacy\local\request\core_userlist_provider,
\core_privacy\local\request\plugin\provider {
/**
* Returns meta data about this system.
@ -126,14 +131,62 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
$contextlist->add_from_sql($sql, ['userid' => $userid, 'analysersamplesorigin' => $analyser->get_samples_origin()]);
}
// We can leave this out of the loop as there is no analyser-dependant stuff.
list($sql, $params) = self::analytics_prediction_actions_sql($userid, array_keys($models));
// We can leave this out of the loop as there is no analyser-dependent stuff.
list($sql, $params) = self::analytics_prediction_actions_user_sql($userid, array_keys($models));
$sql = "SELECT DISTINCT ap.contextid" . $sql;
$contextlist->add_from_sql($sql, $params);
return $contextlist;
}
/**
* Get the list of users who have data within a context.
*
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
*/
public static function get_users_in_context(userlist $userlist) {
global $DB;
$context = $userlist->get_context();
$models = self::get_models_with_user_data();
foreach ($models as $modelid => $model) {
$analyser = $model->get_analyser(['notimesplitting' => true]);
// Analytics predictions.
$params = [
'contextid' => $context->id,
'modelid' => $modelid,
];
$joinusersql = $analyser->join_sample_user('ap');
$sql = "SELECT u.id AS userid
FROM {analytics_predictions} ap
{$joinusersql}
WHERE ap.contextid = :contextid
AND ap.modelid = :modelid";
$userlist->add_from_sql('userid', $sql, $params);
// Indicator calculations.
$params = [
'contextid' => $context->id,
'analysersamplesorigin' => $analyser->get_samples_origin(),
];
$joinusersql = $analyser->join_sample_user('aic');
$sql = "SELECT u.id AS userid
FROM {analytics_indicator_calc} aic
{$joinusersql}
WHERE aic.contextid = :contextid
AND aic.sampleorigin = :analysersamplesorigin";
$userlist->add_from_sql('userid', $sql, $params);
}
// We can leave this out of the loop as there is no analyser-dependent stuff.
list($sql, $params) = self::analytics_prediction_actions_context_sql($context->id, array_keys($models));
$sql = "SELECT apa.userid" . $sql;
$userlist->add_from_sql('userid', $sql, $params);
}
/**
* Export all user data for the specified user, in the specified contexts.
*
@ -215,7 +268,7 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
// Analytics predictions.
// Provided contexts are ignored as we export all user-related stuff.
list($sql, $params) = self::analytics_prediction_actions_sql($userid, $modelids, $contextsql);
list($sql, $params) = self::analytics_prediction_actions_user_sql($userid, $modelids, $contextsql);
$sql = "SELECT apa.*, ap.modelid, ap.contextid, $ctxfields" . $sql;
$predictionactions = $DB->get_recordset_sql($sql, $params + $contextparams);
foreach ($predictionactions as $predictionaction) {
@ -282,7 +335,7 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
list ($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
// Analytics prediction actions.
list($sql, $apaparams) = self::analytics_prediction_actions_sql($userid, $modelids, $contextsql);
list($sql, $apaparams) = self::analytics_prediction_actions_user_sql($userid, $modelids, $contextsql);
$sql = "SELECT apa.id " . $sql;
$predictionactionids = $DB->get_fieldset_sql($sql, $apaparams + $contextparams);
@ -322,6 +375,70 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
}
}
/**
* Delete multiple users within a single context.
*
* @param approved_userlist $userlist The approved context and user information to delete information for.
*/
public static function delete_data_for_users(approved_userlist $userlist) {
global $DB;
$context = $userlist->get_context();
$models = self::get_models_with_user_data();
$modelids = array_keys($models);
list($usersinsql, $baseparams) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED);
// Analytics prediction actions.
list($sql, $apaparams) = self::analytics_prediction_actions_context_sql($context->id, $modelids, $usersinsql);
$sql = "SELECT apa.id" . $sql;
$predictionactionids = $DB->get_fieldset_sql($sql, $baseparams + $apaparams);
if ($predictionactionids) {
list ($predictionactionidssql, $params) = $DB->get_in_or_equal($predictionactionids);
$DB->delete_records_select('analytics_prediction_actions', "id {$predictionactionidssql}", $params);
}
$baseparams['contextid'] = $context->id;
foreach ($models as $modelid => $model) {
$analyser = $model->get_analyser(['notimesplitting' => true]);
// Analytics predictions.
$joinusersql = $analyser->join_sample_user('ap');
$sql = "SELECT DISTINCT ap.id
FROM {analytics_predictions} ap
{$joinusersql}
WHERE ap.contextid = :contextid
AND ap.modelid = :modelid
AND u.id {$usersinsql}";
$params = $baseparams;
$params['modelid'] = $modelid;
$predictionids = $DB->get_fieldset_sql($sql, $params);
if ($predictionids) {
list($predictionidssql, $params) = $DB->get_in_or_equal($predictionids, SQL_PARAMS_NAMED);
$DB->delete_records_select('analytics_predictions', "id {$predictionidssql}", $params);
}
// Indicator calculations.
$joinusersql = $analyser->join_sample_user('aic');
$sql = "SELECT DISTINCT aic.id
FROM {analytics_indicator_calc} aic
{$joinusersql}
WHERE aic.contextid = :contextid
AND aic.sampleorigin = :analysersamplesorigin
AND u.id {$usersinsql}";
$params = $baseparams;
$params['analysersamplesorigin'] = $analyser->get_samples_origin();
$indicatorcalcids = $DB->get_fieldset_sql($sql, $params);
if ($indicatorcalcids) {
list ($indicatorcalcidssql, $params) = $DB->get_in_or_equal($indicatorcalcids, SQL_PARAMS_NAMED);
$DB->delete_records_select('analytics_indicator_calc', "id $indicatorcalcidssql", $params);
}
}
}
/**
* Returns a list of models with user data.
*
@ -339,14 +456,14 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
}
/**
* Returns the sql query to query analytics_prediction_actions table.
* Returns the sql query to query analytics_prediction_actions table by user ID.
*
* @param int $userid
* @param int[] $modelids
* @param string $contextsql
* @return array sql string in [0] and params in [1]
* @param int $userid The user ID of the analytics prediction.
* @param int[] $modelids Model IDs to include in the SQL.
* @param string $contextsql Optional "in or equal" SQL to also query by context ID(s).
* @return array sql string in [0] and params in [1].
*/
private static function analytics_prediction_actions_sql($userid, $modelids, $contextsql = false) {
private static function analytics_prediction_actions_user_sql($userid, $modelids, $contextsql = false) {
global $DB;
list($insql, $params) = $DB->get_in_or_equal($modelids, SQL_PARAMS_NAMED);
@ -363,4 +480,29 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
return [$sql, $params];
}
/**
* Returns the sql query to query analytics_prediction_actions table by context ID.
*
* @param int $contextid The context ID of the analytics prediction.
* @param int[] $modelids Model IDs to include in the SQL.
* @param string $usersql Optional "in or equal" SQL to also query by user ID(s).
* @return array sql string in [0] and params in [1].
*/
private static function analytics_prediction_actions_context_sql($contextid, $modelids, $usersql = false) {
global $DB;
list($insql, $params) = $DB->get_in_or_equal($modelids, SQL_PARAMS_NAMED);
$sql = " FROM {analytics_predictions} ap
JOIN {analytics_prediction_actions} apa ON apa.predictionid = ap.id
WHERE ap.contextid = :contextid
AND ap.modelid {$insql}";
$params['contextid'] = $contextid;
if ($usersql) {
$sql .= " AND apa.userid {$usersql}";
}
return [$sql, $params];
}
}

View File

@ -26,6 +26,7 @@ use \core_analytics\privacy\provider;
use core_privacy\local\request\transform;
use core_privacy\local\request\writer;
use core_privacy\local\request\approved_contextlist;
use core_privacy\local\request\approved_userlist;
defined('MOODLE_INTERNAL') || die();
@ -99,6 +100,42 @@ class core_analytics_privacy_model_testcase extends \core_privacy\tests\provider
$this->setAdminUser();
}
/**
* Test fetching users within a context.
*/
public function test_get_users_in_context() {
global $CFG;
$component = 'core_analytics';
$course1context = \context_course::instance($this->c1->id);
$course2context = \context_course::instance($this->c2->id);
$systemcontext = \context_system::instance();
$expected = [$this->u1->id, $this->u2->id, $this->u3->id, $this->u4->id];
// Check users exist in the relevant contexts.
$userlist = new \core_privacy\local\request\userlist($course1context, $component);
provider::get_users_in_context($userlist);
$actual = $userlist->get_userids();
sort($actual);
$this->assertEquals($expected, $actual);
$userlist = new \core_privacy\local\request\userlist($course2context, $component);
provider::get_users_in_context($userlist);
$actual = $userlist->get_userids();
sort($actual);
$this->assertEquals($expected, $actual);
// System context will also find guest and admin user, add to expected before testing.
$expected = array_merge($expected, [$CFG->siteguest, get_admin()->id]);
sort($expected);
$userlist = new \core_privacy\local\request\userlist($systemcontext, $component);
provider::get_users_in_context($userlist);
$actual = $userlist->get_userids();
sort($actual);
$this->assertEquals($expected, $actual);
}
/**
* Test delete a context.
*
@ -160,6 +197,130 @@ class core_analytics_privacy_model_testcase extends \core_privacy\tests\provider
$this->assertEquals(0, $DB->count_records('analytics_predictions'));
}
/**
* Test deleting multiple users in a context.
*/
public function test_delete_data_for_users() {
global $DB;
$component = 'core_analytics';
$course1context = \context_course::instance($this->c1->id);
$course2context = \context_course::instance($this->c2->id);
$systemcontext = \context_system::instance();
// Ensure all records exist in expected contexts.
$expectedcontexts = [$course1context->id, $course2context->id, $systemcontext->id];
sort($expectedcontexts);
$actualcontexts = [
$this->u1->id => provider::get_contexts_for_userid($this->u1->id)->get_contextids(),
$this->u2->id => provider::get_contexts_for_userid($this->u2->id)->get_contextids(),
$this->u3->id => provider::get_contexts_for_userid($this->u3->id)->get_contextids(),
$this->u4->id => provider::get_contexts_for_userid($this->u4->id)->get_contextids(),
];
foreach ($actualcontexts as $userid => $unused) {
sort($actualcontexts[$userid]);
$this->assertEquals($expectedcontexts, $actualcontexts[$userid]);
}
// Test initial record counts are as expected.
$this->assertEquals(6, $DB->count_records('analytics_predictions'));
$this->assertEquals(1, $DB->count_records('analytics_prediction_actions'));
$this->assertEquals(14, $DB->count_records('analytics_indicator_calc'));
// Delete u1 and u3 from system context.
$approveduserids = [$this->u1->id, $this->u3->id];
$approvedlist = new approved_userlist($systemcontext, $component, $approveduserids);
provider::delete_data_for_users($approvedlist);
// Ensure u1 and u3 system context data deleted only.
$expectedcontexts = [
$this->u1->id => [$course1context->id, $course2context->id],
$this->u2->id => [$systemcontext->id, $course1context->id, $course2context->id],
$this->u3->id => [$course1context->id, $course2context->id],
$this->u4->id => [$systemcontext->id, $course1context->id, $course2context->id],
];
$actualcontexts = [
$this->u1->id => provider::get_contexts_for_userid($this->u1->id)->get_contextids(),
$this->u2->id => provider::get_contexts_for_userid($this->u2->id)->get_contextids(),
$this->u3->id => provider::get_contexts_for_userid($this->u3->id)->get_contextids(),
$this->u4->id => provider::get_contexts_for_userid($this->u4->id)->get_contextids(),
];
foreach ($actualcontexts as $userid => $unused) {
sort($expectedcontexts[$userid]);
sort($actualcontexts[$userid]);
$this->assertEquals($expectedcontexts[$userid], $actualcontexts[$userid]);
}
// Test expected number of records have been deleted.
$this->assertEquals(5, $DB->count_records('analytics_predictions'));
$this->assertEquals(1, $DB->count_records('analytics_prediction_actions'));
$this->assertEquals(12, $DB->count_records('analytics_indicator_calc'));
// Delete for all 4 users in course 2 context.
$approveduserids = [$this->u1->id, $this->u2->id, $this->u3->id, $this->u4->id];
$approvedlist = new approved_userlist($course2context, $component, $approveduserids);
provider::delete_data_for_users($approvedlist);
// Ensure all course 2 context data deleted for all 4 users.
$expectedcontexts = [
$this->u1->id => [$course1context->id],
$this->u2->id => [$systemcontext->id, $course1context->id],
$this->u3->id => [$course1context->id],
$this->u4->id => [$systemcontext->id, $course1context->id],
];
$actualcontexts = [
$this->u1->id => provider::get_contexts_for_userid($this->u1->id)->get_contextids(),
$this->u2->id => provider::get_contexts_for_userid($this->u2->id)->get_contextids(),
$this->u3->id => provider::get_contexts_for_userid($this->u3->id)->get_contextids(),
$this->u4->id => provider::get_contexts_for_userid($this->u4->id)->get_contextids(),
];
foreach ($actualcontexts as $userid => $unused) {
sort($actualcontexts[$userid]);
sort($expectedcontexts[$userid]);
$this->assertEquals($expectedcontexts[$userid], $actualcontexts[$userid]);
}
// Test expected number of records have been deleted.
$this->assertEquals(3, $DB->count_records('analytics_predictions'));
$this->assertEquals(1, $DB->count_records('analytics_prediction_actions'));
$this->assertEquals(8, $DB->count_records('analytics_indicator_calc'));
$approveduserids = [$this->u3->id];
$approvedlist = new approved_userlist($course1context, $component, $approveduserids);
provider::delete_data_for_users($approvedlist);
// Ensure all course 1 context data deleted for u3.
$expectedcontexts = [
$this->u1->id => [$course1context->id],
$this->u2->id => [$systemcontext->id, $course1context->id],
$this->u3->id => [],
$this->u4->id => [$systemcontext->id, $course1context->id],
];
$actualcontexts = [
$this->u1->id => provider::get_contexts_for_userid($this->u1->id)->get_contextids(),
$this->u2->id => provider::get_contexts_for_userid($this->u2->id)->get_contextids(),
$this->u3->id => provider::get_contexts_for_userid($this->u3->id)->get_contextids(),
$this->u4->id => provider::get_contexts_for_userid($this->u4->id)->get_contextids(),
];
foreach ($actualcontexts as $userid => $unused) {
sort($actualcontexts[$userid]);
sort($expectedcontexts[$userid]);
$this->assertEquals($expectedcontexts[$userid], $actualcontexts[$userid]);
}
// Test expected number of records have been deleted.
$this->assertEquals(2, $DB->count_records('analytics_predictions'));
$this->assertEquals(0, $DB->count_records('analytics_prediction_actions'));
$this->assertEquals(7, $DB->count_records('analytics_indicator_calc'));
}
/**
* Test export user data.
*