Merge branch 'MDL-62218-master' of git://github.com/ryanwyllie/moodle

This commit is contained in:
Andrew Nicols 2018-05-04 11:11:14 +08:00
commit 2cae92c6c3
13 changed files with 1131 additions and 0 deletions

View File

@ -434,6 +434,53 @@ abstract class base {
return $this->log;
}
/**
* Whether the plugin needs user data clearing or not.
*
* This is related to privacy. Override this method if your analyser samples have any relation
* to the 'user' database entity. We need to clean the site from all user-related data if a user
* request their data to be deleted from the system. A static::provided_sample_data returning 'user'
* is an indicator that you should be returning true.
*
* @return bool
*/
public function processes_user_data() {
return false;
}
/**
* SQL JOIN from a sample to users table.
*
* This function should be defined if static::processes_user_data returns true and it is related to analytics API
* privacy API implementation. It allows the analytics API to identify data associated to users that needs to be
* deleted or exported.
*
* This function receives the alias of a table with a 'sampleid' field and it should return a SQL join
* with static::get_samples_origin and with 'user' table. Note that:
* - The function caller expects the returned 'user' table to be aliased as 'u' (defacto standard in moodle).
* - You can join with other tables if your samples origin table does not contain a 'userid' field (if that would be
* a requirement this solution would be automated for you) you can't though use the following
* aliases: 'ap', 'apa', 'aic' and 'am'.
*
* Some examples:
*
* static::get_samples_origin() === 'user':
* JOIN {user} u ON {$sampletablealias}.sampleid = u.id
*
* static::get_samples_origin() === 'role_assignments':
* JOIN {role_assignments} ra ON {$sampletablealias}.sampleid = ra.userid JOIN {user} u ON u.id = ra.userid
*
* static::get_samples_origin() === 'user_enrolments':
* JOIN {user_enrolments} ue ON {$sampletablealias}.sampleid = ue.userid JOIN {user} u ON u.id = ue.userid
*
* @throws \coding_exception
* @param string $sampletablealias The alias of the table with a sampleid field that will join with this SQL string
* @return string
*/
public function join_sample_user($sampletablealias) {
throw new \coding_exception('This method should be implemented if static::processes_user_data returns true.');
}
/**
* Processes the analysable samples using the provided time splitting method.
*

View File

@ -0,0 +1,365 @@
<?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/>.
/**
* Privacy Subsystem implementation for core_analytics.
*
* @package core_analytics
* @copyright 2018 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_analytics\privacy;
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\context;
use core_privacy\local\request\contextlist;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for core_analytics implementing metadata and plugin providers.
*
* @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 {
/**
* Returns meta data about this system.
*
* @param collection $collection The initialised collection to add items to.
* @return collection A listing of user data stored through this system.
*/
public static function get_metadata(collection $collection) : collection {
$collection->add_database_table(
'analytics_indicator_calc',
[
'starttime' => 'privacy:metadata:analytics:indicatorcalc:starttime',
'endtime' => 'privacy:metadata:analytics:indicatorcalc:endtime',
'contextid' => 'privacy:metadata:analytics:indicatorcalc:contextid',
'sampleorigin' => 'privacy:metadata:analytics:indicatorcalc:sampleorigin',
'sampleid' => 'privacy:metadata:analytics:indicatorcalc:sampleid',
'indicator' => 'privacy:metadata:analytics:indicatorcalc:indicator',
'value' => 'privacy:metadata:analytics:indicatorcalc:value',
'timecreated' => 'privacy:metadata:analytics:indicatorcalc:timecreated',
],
'privacy:metadata:analytics:indicatorcalc'
);
$collection->add_database_table(
'analytics_predictions',
[
'modelid' => 'privacy:metadata:analytics:predictions:modelid',
'contextid' => 'privacy:metadata:analytics:predictions:contextid',
'sampleid' => 'privacy:metadata:analytics:predictions:sampleid',
'rangeindex' => 'privacy:metadata:analytics:predictions:rangeindex',
'prediction' => 'privacy:metadata:analytics:predictions:prediction',
'predictionscore' => 'privacy:metadata:analytics:predictions:predictionscore',
'calculations' => 'privacy:metadata:analytics:predictions:calculations',
'timecreated' => 'privacy:metadata:analytics:predictions:timecreated',
'timestart' => 'privacy:metadata:analytics:predictions:timestart',
'timeend' => 'privacy:metadata:analytics:predictions:timeend',
],
'privacy:metadata:analytics:predictions'
);
$collection->add_database_table(
'analytics_prediction_actions',
[
'predictionid' => 'privacy:metadata:analytics:predictionactions:predictionid',
'userid' => 'privacy:metadata:analytics:predictionactions:userid',
'actionname' => 'privacy:metadata:analytics:predictionactions:actionname',
'timecreated' => 'privacy:metadata:analytics:predictionactions:timecreated',
],
'privacy:metadata:analytics:predictionactions'
);
return $collection;
}
/**
* Get the list of contexts that contain user information for the specified user.
*
* @param int $userid The user to search.
* @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
*/
public static function get_contexts_for_userid(int $userid) : contextlist {
global $DB;
$contextlist = new \core_privacy\local\request\contextlist();
$models = self::get_models_with_user_data();
foreach ($models as $modelid => $model) {
$analyser = $model->get_analyser(['notimesplitting' => true]);
// Analytics predictions.
$joinusersql = $analyser->join_sample_user('ap');
$sql = "SELECT DISTINCT ap.contextid FROM {analytics_predictions} ap
{$joinusersql}
WHERE u.id = :userid AND ap.modelid = :modelid";
$contextlist->add_from_sql($sql, ['userid' => $userid, 'modelid' => $modelid]);
// Indicator calculations.
$joinusersql = $analyser->join_sample_user('aic');
$sql = "SELECT DISTINCT aic.contextid FROM {analytics_indicator_calc} aic
{$joinusersql}
WHERE u.id = :userid";
$contextlist->add_from_sql($sql, ['userid' => $userid]);
}
// 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));
$sql = "SELECT DISTINCT ap.contextid" . $sql;
$contextlist->add_from_sql($sql, $params);
return $contextlist;
}
/**
* Export all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts to export information for.
*/
public static function export_user_data(approved_contextlist $contextlist) {
global $DB;
$userid = intval($contextlist->get_user()->id);
$models = self::get_models_with_user_data();
$modelids = array_keys($models);
list ($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
$rootpath = [get_string('analytics', 'analytics')];
$ctxfields = \context_helper::get_preload_record_columns_sql('ctx');
foreach ($models as $modelid => $model) {
$analyser = $model->get_analyser(['notimesplitting' => true]);
// Analytics predictions.
$joinusersql = $analyser->join_sample_user('ap');
$sql = "SELECT ap.*, $ctxfields FROM {analytics_predictions} ap
JOIN {context} ctx ON ctx.id = ap.contextid
{$joinusersql}
WHERE u.id = :userid AND ap.modelid = :modelid AND ap.contextid {$contextsql}";
$params = ['userid' => $userid, 'modelid' => $modelid] + $contextparams;
$predictions = $DB->get_recordset_sql($sql, $params);
foreach ($predictions as $prediction) {
\context_helper::preload_from_record($prediction);
$context = \context::instance_by_id($prediction->contextid);
$path = $rootpath;
$path[] = get_string('privacy:metadata:analytics:predictions', 'analytics');
$path[] = $prediction->id;
$data = (object)[
'target' => $model->get_target()->get_name()->out(),
'context' => $context->get_context_name(true, true),
'prediction' => $model->get_target()->get_display_value($prediction->prediction),
'timestart' => transform::datetime($prediction->timestart),
'timeend' => transform::datetime($prediction->timeend),
'timecreated' => transform::datetime($prediction->timecreated),
];
writer::with_context($context)->export_data($path, $data);
}
$predictions->close();
// Indicator calculations.
$joinusersql = $analyser->join_sample_user('aic');
$sql = "SELECT aic.*, $ctxfields FROM {analytics_indicator_calc} aic
JOIN {context} ctx ON ctx.id = aic.contextid
{$joinusersql}
WHERE u.id = :userid AND aic.contextid {$contextsql}";
$params = ['userid' => $userid] + $contextparams;
$indicatorcalculations = $DB->get_recordset_sql($sql, $params);
foreach ($indicatorcalculations as $calculation) {
\context_helper::preload_from_record($calculation);
$context = \context::instance_by_id($calculation->contextid);
$path = $rootpath;
$path[] = get_string('privacy:metadata:analytics:indicatorcalc', 'analytics');
$path[] = $calculation->id;
$indicator = \core_analytics\manager::get_indicator($calculation->indicator);
$data = (object)[
'indicator' => $indicator::get_name()->out(),
'context' => $context->get_context_name(true, true),
'calculation' => $indicator->get_display_value($calculation->value),
'starttime' => transform::datetime($calculation->starttime),
'endtime' => transform::datetime($calculation->endtime),
'timecreated' => transform::datetime($calculation->timecreated),
];
writer::with_context($context)->export_data($path, $data);
}
$indicatorcalculations->close();
}
// Analytics predictions.
// Provided contexts are ignored as we export all user-related stuff.
list($sql, $params) = self::analytics_prediction_actions_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) {
\context_helper::preload_from_record($predictionaction);
$context = \context::instance_by_id($predictionaction->contextid);
$path = $rootpath;
$path[] = get_string('privacy:metadata:analytics:predictionactions', 'analytics');
$path[] = $predictionaction->id;
$data = (object)[
'target' => $models[$predictionaction->modelid]->get_target()->get_name()->out(),
'context' => $context->get_context_name(true, true),
'action' => $predictionaction->actionname,
'timecreated' => transform::datetime($predictionaction->timecreated),
];
writer::with_context($context)->export_data($path, $data);
}
$predictionactions->close();
}
/**
* Delete all data for all users in the specified context.
*
* @param context $context The specific context to delete data for.
*/
public static function delete_data_for_all_users_in_context(\context $context) {
global $DB;
$models = self::get_models_with_user_data();
$modelids = array_keys($models);
foreach ($models as $modelid => $model) {
$idssql = "SELECT ap.id FROM {analytics_predictions} ap
WHERE ap.contextid = :contextid AND ap.modelid = :modelid";
$idsparams = ['contextid' => $context->id, 'modelid' => $modelid];
$predictionids = $DB->get_fieldset_sql($idssql, $idsparams);
if ($predictionids) {
list($predictionidssql, $params) = $DB->get_in_or_equal($predictionids, SQL_PARAMS_NAMED);
$DB->delete_records_select('analytics_prediction_actions', "predictionid IN ($idssql)", $idsparams);
$DB->delete_records_select('analytics_predictions', "id $predictionidssql", $params);
}
}
// We delete them all this table is just a cache and we don't know which model filled it.
$DB->delete_records('analytics_indicator_calc', ['contextid' => $context->id]);
}
/**
* Delete all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
*/
public static function delete_data_for_user(approved_contextlist $contextlist) {
global $DB;
$userid = intval($contextlist->get_user()->id);
$models = self::get_models_with_user_data();
$modelids = array_keys($models);
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);
$sql = "SELECT apa.id " . $sql;
$predictionactionids = $DB->get_fieldset_sql($sql, $apaparams + $contextparams);
if ($predictionactionids) {
list ($predictionactionidssql, $params) = $DB->get_in_or_equal($predictionactionids);
$DB->delete_records_select('analytics_prediction_actions', "id {$predictionactionidssql}", $params);
}
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 u.id = :userid AND ap.modelid = :modelid AND ap.contextid {$contextsql}";
$predictionids = $DB->get_fieldset_sql($sql, ['userid' => $userid, 'modelid' => $modelid] + $contextparams);
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 u.id = :userid AND aic.contextid {$contextsql}";
$indicatorcalcids = $DB->get_fieldset_sql($sql, ['userid' => $userid] + $contextparams);
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.
*
* @return \core_analytics\model[]
*/
private static function get_models_with_user_data() {
$models = \core_analytics\manager::get_all_models();
foreach ($models as $modelid => $model) {
$analyser = $model->get_analyser(['notimesplitting' => true]);
if (!$analyser->processes_user_data()) {
unset($models[$modelid]);
}
}
return $models;
}
/**
* Returns the sql query to query analytics_prediction_actions table.
*
* @param int $userid
* @param int[] $modelids
* @param string $contextsql
* @return array sql string in [0] and params in [1]
*/
private static function analytics_prediction_actions_sql($userid, $modelids, $contextsql = false) {
global $DB;
list($insql, $params) = $DB->get_in_or_equal($modelids, SQL_PARAMS_NAMED);
$sql = " FROM {analytics_predictions} ap
JOIN {context} ctx ON ctx.id = ap.contextid
JOIN {analytics_prediction_actions} apa ON apa.predictionid = ap.id
JOIN {analytics_models} am ON ap.modelid = am.id
WHERE apa.userid = :userid AND ap.modelid {$insql}";
$params['userid'] = $userid;
if ($contextsql) {
$sql .= " AND ap.contextid $contextsql";
}
return [$sql, $params];
}
}

View File

@ -0,0 +1,147 @@
<?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/>.
/**
* Test analyser
*
* @package core
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Test analyser
*
* @package core
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class test_site_users_analyser extends \core_analytics\local\analyser\sitewide {
/**
* Samples origin is course table.
*
* @return string
*/
public function get_samples_origin() {
return 'user';
}
/**
* Returns the sample analysable
*
* @param int $sampleid
* @return \core_analytics\analysable
*/
public function get_sample_analysable($sampleid) {
return new \core_analytics\site();
}
/**
* Data this analyer samples provide.
*
* @return string[]
*/
protected function provided_sample_data() {
return array('user');
}
/**
* Returns the sample context.
*
* @param int $sampleid
* @return \context
*/
public function sample_access_context($sampleid) {
return \context_system::instance();
}
/**
* Returns all site courses.
*
* @param \core_analytics\analysable $site
* @return array
*/
protected function get_all_samples(\core_analytics\analysable $site) {
global $DB;
$users = $DB->get_records('user');
$userids = array_keys($users);
$sampleids = array_combine($userids, $userids);
$users = array_map(function($user) {
return array('user' => $user);
}, $users);
return array($sampleids, $users);
}
/**
* Return all complete samples data from sample ids.
*
* @param int[] $sampleids
* @return array
*/
public function get_samples($sampleids) {
global $DB;
list($userssql, $params) = $DB->get_in_or_equal($sampleids, SQL_PARAMS_NAMED);
$users = $DB->get_records_select('user', "id {$userssql}", $params);
$userids = array_keys($users);
$sampleids = array_combine($userids, $userids);
$users = array_map(function($user) {
return array('user' => $user);
}, $users);
return array($sampleids, $users);
}
/**
* Returns the description of a sample.
*
* @param int $sampleid
* @param int $contextid
* @param array $sampledata
* @return array array(string, \renderable)
*/
public function sample_description($sampleid, $contextid, $sampledata) {
$description = fullname($samplesdata['user']);
$userimage = new \pix_icon('i/user', get_string('user'));
return array($description, $userimage);
}
/**
* We need to delete associated data if a user requests his data to be deleted.
*
* @return bool
*/
public function processes_user_data() {
return true;
}
/**
* Join the samples origin table with the user id table.
*
* @param string $sampletablealias
* @return string
*/
public function join_sample_user($sampletablealias) {
return "JOIN {user} u ON u.id = {$sampletablealias}.sampleid";
}
}

View File

@ -0,0 +1,60 @@
<?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/>.
/**
* Test target.
*
* @package core_analytics
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/test_target_shortname.php');
/**
* Test target.
*
* @package core_analytics
* @copyright 2018 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class test_target_course_users extends test_target_site_users {
/**
* get_analyser_class
*
* @return string
*/
public function get_analyser_class() {
return '\core\analytics\analyser\student_enrolments';
}
/**
* Returns a lang_string object representing the name for the indicator.
*
* Used as column identificator.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name() : \lang_string {
// Using a string that exists and contains a corresponding '_help' string.
return new \lang_string('adminhelpedituser');
}
}

View File

@ -0,0 +1,151 @@
<?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/>.
/**
* Test target.
*
* @package core_analytics
* @copyright 2018 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/test_site_users_analyser.php');
/**
* Test target.
*
* @package core_analytics
* @copyright 2018 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class test_target_site_users extends \core_analytics\local\target\binary {
/**
* Returns a lang_string object representing the name for the indicator.
*
* Used as column identificator.
*
* If there is a corresponding '_help' string this will be shown as well.
*
* @return \lang_string
*/
public static function get_name() : \lang_string {
// Using a string that exists and contains a corresponding '_help' string.
return new \lang_string('adminhelplogs');
}
/**
* predictions
*
* @var array
*/
protected $predictions = array();
/**
* get_analyser_class
*
* @return string
*/
public function get_analyser_class() {
return 'test_site_users_analyser';
}
/**
* classes_description
*
* @return string[]
*/
public static function classes_description() {
return array(
'firstname first char is A',
'firstname first char is not A'
);
}
/**
* We don't want to discard results.
* @return float
*/
protected function min_prediction_score() {
return null;
}
/**
* We don't want to discard results.
* @return array
*/
protected function ignored_predicted_classes() {
return array();
}
/**
* is_valid_analysable
*
* @param \core_analytics\analysable $analysable
* @param bool $fortraining
* @return bool
*/
public function is_valid_analysable(\core_analytics\analysable $analysable, $fortraining = true) {
// This is testing, let's make things easy.
return true;
}
/**
* is_valid_sample
*
* @param int $sampleid
* @param \core_analytics\analysable $analysable
* @param bool $fortraining
* @return bool
*/
public function is_valid_sample($sampleid, \core_analytics\analysable $analysable, $fortraining = true) {
// We skip not-visible courses during training as a way to emulate the training data / prediction data difference.
// In normal circumstances is_valid_sample will return false when they receive a sample that can not be
// processed.
if (!$fortraining) {
return true;
}
$sample = $this->retrieve('user', $sampleid);
if ($sample->lastname == 'b') {
return false;
}
return true;
}
/**
* calculate_sample
*
* @param int $sampleid
* @param \core_analytics\analysable $analysable
* @param int $starttime
* @param int $endtime
* @return float
*/
protected function calculate_sample($sampleid, \core_analytics\analysable $analysable, $starttime = false, $endtime = false) {
$sample = $this->retrieve('user', $sampleid);
$firstchar = substr($sample->firstname, 0, 1);
if ($firstchar === 'a') {
return 1;
} else {
return 0;
}
}
}

View File

@ -0,0 +1,213 @@
<?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/>.
/**
* Unit tests for privacy.
*
* @package core_analytics
* @copyright 2018 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use \core_analytics\privacy\provider;
use core_privacy\local\request\transform;
use core_privacy\local\request\writer;
use core_privacy\local\request\approved_contextlist;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/fixtures/test_indicator_max.php');
require_once(__DIR__ . '/fixtures/test_indicator_min.php');
require_once(__DIR__ . '/fixtures/test_target_site_users.php');
require_once(__DIR__ . '/fixtures/test_target_course_users.php');
require_once(__DIR__ . '/fixtures/test_analyser.php');
/**
* Unit tests for privacy.
*
* @package core_analytics
* @copyright 2018 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class core_analytics_privacy_model_testcase extends \core_privacy\tests\provider_testcase {
public function setUp() {
$this->resetAfterTest(true);
$this->setAdminUser();
$timesplittingid = '\core\analytics\time_splitting\single_range';
$target = \core_analytics\manager::get_target('test_target_site_users');
$indicators = array('test_indicator_max');
foreach ($indicators as $key => $indicator) {
$indicators[$key] = \core_analytics\manager::get_indicator($indicator);
}
$this->model1 = \core_analytics\model::create($target, $indicators, $timesplittingid);
$this->modelobj1 = $this->model1->get_model_obj();
$target = \core_analytics\manager::get_target('test_target_course_users');
$indicators = array('test_indicator_min');
foreach ($indicators as $key => $indicator) {
$indicators[$key] = \core_analytics\manager::get_indicator($indicator);
}
$this->model2 = \core_analytics\model::create($target, $indicators, $timesplittingid);
$this->modelobj2 = $this->model1->get_model_obj();
$this->u1 = $this->getDataGenerator()->create_user(['firstname' => 'a111111111111', 'lastname' => 'a']);
$this->u2 = $this->getDataGenerator()->create_user(['firstname' => 'a222222222222', 'lastname' => 'a']);
$this->u3 = $this->getDataGenerator()->create_user(['firstname' => 'b333333333333', 'lastname' => 'b']);
$this->u4 = $this->getDataGenerator()->create_user(['firstname' => 'b444444444444', 'lastname' => 'b']);
$this->c1 = $this->getDataGenerator()->create_course(['visible' => false]);
$this->c2 = $this->getDataGenerator()->create_course();
$this->getDataGenerator()->enrol_user($this->u1->id, $this->c1->id, 'student');
$this->getDataGenerator()->enrol_user($this->u2->id, $this->c1->id, 'student');
$this->getDataGenerator()->enrol_user($this->u3->id, $this->c1->id, 'student');
$this->getDataGenerator()->enrol_user($this->u4->id, $this->c1->id, 'student');
$this->getDataGenerator()->enrol_user($this->u1->id, $this->c2->id, 'student');
$this->getDataGenerator()->enrol_user($this->u2->id, $this->c2->id, 'student');
$this->getDataGenerator()->enrol_user($this->u3->id, $this->c2->id, 'student');
$this->getDataGenerator()->enrol_user($this->u4->id, $this->c2->id, 'student');
$this->setAdminUser();
$this->model1->train();
$this->model1->predict();
$this->model2->train();
$this->model2->predict();
list($total, $predictions) = $this->model2->get_predictions(\context_course::instance($this->c1->id));
$this->setUser($this->u3);
$prediction = reset($predictions);
$prediction->action_executed('notuseful', $this->model2->get_target());
$this->setAdminUser();
}
/**
* Test delete a context.
*
* @return null
*/
public function test_delete_context_data() {
global $DB;
// We have 2 predictions for model1 and 4 predictions for model2.
$this->assertEquals(6, $DB->count_records('analytics_predictions'));
$this->assertEquals(14, $DB->count_records('analytics_indicator_calc'));
// We have 1 prediction action.
$this->assertEquals(1, $DB->count_records('analytics_prediction_actions'));
$coursecontext = \context_course::instance($this->c1->id);
// Delete the course that was used for prediction.
provider::delete_data_for_all_users_in_context($coursecontext);
// The course predictions are deleted.
$this->assertEquals(4, $DB->count_records('analytics_predictions'));
// Calculations related to that context are deleted.
$this->assertEmpty($DB->count_records('analytics_indicator_calc', ['contextid' => $coursecontext->id]));
// The deleted context prediction actions are deleted as well.
$this->assertEquals(0, $DB->count_records('analytics_prediction_actions'));
}
/**
* Test delete a user.
*
* @return null
*/
public function test_delete_user_data() {
global $DB;
$usercontexts = provider::get_contexts_for_userid($this->u3->id);
$contextlist = new \core_privacy\local\request\approved_contextlist($this->u3, 'core_analytics',
$usercontexts->get_contextids());
provider::delete_data_for_user($contextlist);
// The site level prediction for u3 was deleted.
$this->assertEquals(3, $DB->count_records('analytics_predictions'));
$this->assertEquals(0, $DB->count_records('analytics_prediction_actions'));
$usercontexts = provider::get_contexts_for_userid($this->u1->id);
$contextlist = new \core_privacy\local\request\approved_contextlist($this->u1, 'core_analytics',
$usercontexts->get_contextids());
provider::delete_data_for_user($contextlist);
// We have nothing for u1.
$this->assertEquals(3, $DB->count_records('analytics_predictions'));
$usercontexts = provider::get_contexts_for_userid($this->u4->id);
$contextlist = new \core_privacy\local\request\approved_contextlist($this->u4, 'core_analytics',
$usercontexts->get_contextids());
provider::delete_data_for_user($contextlist);
$this->assertEquals(0, $DB->count_records('analytics_predictions'));
}
/**
* Test export user data.
*
* @return null
*/
public function test_export_data() {
global $DB;
$system = \context_system::instance();
list($total, $predictions) = $this->model1->get_predictions($system);
foreach ($predictions as $key => $prediction) {
if ($prediction->get_prediction_data()->sampleid !== $this->u3->id) {
$otheruserprediction = $prediction;
break;
}
}
$this->setUser($this->u3);
$otheruserprediction->action_executed('notuseful', $this->model1->get_target());
$this->setAdminUser();
$this->export_context_data_for_user($this->u3->id, $system, 'core_analytics');
$writer = \core_privacy\local\request\writer::with_context($system);
$this->assertTrue($writer->has_any_data());
$u3prediction = $DB->get_record('analytics_predictions', ['contextid' => $system->id, 'sampleid' => $this->u3->id]);
$data = $writer->get_data([get_string('analytics', 'analytics'),
get_string('privacy:metadata:analytics:predictions', 'analytics'), $u3prediction->id]);
$this->assertEquals(get_string('adminhelplogs'), $data->target);
$this->assertEquals(get_string('coresystem'), $data->context);
$this->assertEquals('firstname first char is not A', $data->prediction);
$u3calculation = $DB->get_record('analytics_indicator_calc', ['contextid' => $system->id, 'sampleid' => $this->u3->id]);
$data = $writer->get_data([get_string('analytics', 'analytics'),
get_string('privacy:metadata:analytics:indicatorcalc', 'analytics'), $u3calculation->id]);
$this->assertEquals('Allow stealth activities', $data->indicator);
$this->assertEquals(get_string('coresystem'), $data->context);
$this->assertEquals(get_string('yes'), $data->calculation);
$sql = "SELECT apa.id FROM {analytics_prediction_actions} apa
JOIN {analytics_predictions} ap ON ap.id = apa.predictionid
WHERE ap.contextid = :contextid AND apa.userid = :userid AND ap.modelid = :modelid";
$params = ['contextid' => $system->id, 'userid' => $this->u3->id, 'modelid' => $this->model1->get_id()];
$u3action = $DB->get_record_sql($sql, $params);
$data = $writer->get_data([get_string('analytics', 'analytics'),
get_string('privacy:metadata:analytics:predictionactions', 'analytics'), $u3action->id]);
$this->assertEquals(get_string('adminhelplogs'), $data->target);
$this->assertEquals(get_string('coresystem'), $data->context);
$this->assertEquals('notuseful', $data->action);
}
}

9
analytics/upgrade.txt Normal file
View File

@ -0,0 +1,9 @@
This files describes API changes in analytics sub system,
information provided here is intended especially for developers.
=== 3.5 ===
* There are two new methods for analysers, processes_user_data() and join_sample_user(). You
need to overwrite them if your analyser uses user data. As a general statement, you should
overwrite these new methods if your samples return 'user' data. These new methods are used
for analytics' privacy API implementation.

View File

@ -83,6 +83,31 @@ $string['onlycli'] = 'Analytics processes execution via command line only';
$string['onlycliinfo'] = 'Analytics processes like evaluating models, training machine learning algorithms or getting predictions can take some time, they will run as cron tasks and they can be forced via command line. Disable this setting if you want your site managers to be able to run these processes manually via web interface';
$string['predictionsprocessor'] = 'Predictions processor';
$string['predictionsprocessor_help'] = 'A predictions processor is the machine-learning backend that processes the datasets generated by calculating models\' indicators and targets. All trained algorithms and predictions will be deleted if you change to another predictions processor.';
$string['privacy:metadata:analytics:indicatorcalc'] = 'Indicator calculations';
$string['privacy:metadata:analytics:indicatorcalc:starttime'] = 'Calculation start time';
$string['privacy:metadata:analytics:indicatorcalc:endtime'] = 'Calculation end time';
$string['privacy:metadata:analytics:indicatorcalc:contextid'] = 'The context';
$string['privacy:metadata:analytics:indicatorcalc:sampleorigin'] = 'The origin table of the sample';
$string['privacy:metadata:analytics:indicatorcalc:sampleid'] = 'The sample id';
$string['privacy:metadata:analytics:indicatorcalc:indicator'] = 'The indicator calculator class';
$string['privacy:metadata:analytics:indicatorcalc:value'] = 'The calculated value';
$string['privacy:metadata:analytics:indicatorcalc:timecreated'] = 'When the prediction was made';
$string['privacy:metadata:analytics:predictions'] = 'Predictions';
$string['privacy:metadata:analytics:predictions:modelid'] = 'The model id';
$string['privacy:metadata:analytics:predictions:contextid'] = 'The context';
$string['privacy:metadata:analytics:predictions:sampleid'] = 'The sample id';
$string['privacy:metadata:analytics:predictions:rangeindex'] = 'The index of the time splitting method';
$string['privacy:metadata:analytics:predictions:prediction'] = 'The prediction';
$string['privacy:metadata:analytics:predictions:predictionscore'] = 'The prediction score';
$string['privacy:metadata:analytics:predictions:calculations'] = 'Indicator calculations';
$string['privacy:metadata:analytics:predictions:timecreated'] = 'When the prediction was made';
$string['privacy:metadata:analytics:predictions:timestart'] = 'Calculations time start';
$string['privacy:metadata:analytics:predictions:timeend'] = 'Calculations time end';
$string['privacy:metadata:analytics:predictionactions'] = 'Prediction actions';
$string['privacy:metadata:analytics:predictionactions:predictionid'] = 'The prediction id';
$string['privacy:metadata:analytics:predictionactions:userid'] = 'The user that made the action';
$string['privacy:metadata:analytics:predictionactions:actionname'] = 'The action name';
$string['privacy:metadata:analytics:predictionactions:timecreated'] = 'When the prediction action was performed';
$string['processingsitecontents'] = 'Processing site contents';
$string['successfullyanalysed'] = 'Successfully analysed';
$string['timesplittingmethod'] = 'Time-splitting method';

View File

@ -83,6 +83,26 @@ class student_enrolments extends \core_analytics\local\analyser\by_course {
return array('user_enrolments', 'context', 'course', 'user');
}
/**
* We need to delete associated data if a user requests his data to be deleted.
*
* @return bool
*/
public function processes_user_data() {
return true;
}
/**
* Join the samples origin table with the user id table.
*
* @param string $sampletablealias
* @return string
*/
public function join_sample_user($sampletablealias) {
return "JOIN {user_enrolments} ue ON {$sampletablealias}.sampleid = ue.id " .
"JOIN {user} u ON u.id = ue.userid";
}
/**
* All course student enrolments.
*

View File

@ -0,0 +1,46 @@
<?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/>.
/**
* Privacy Subsystem implementation for mlbackend_php.
*
* @package mlbackend_php
* @copyright 2018 David Monllao
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mlbackend_php\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for mlbackend_php implementing null_provider.
*
* @copyright 2018 David Monllao
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements \core_privacy\local\metadata\null_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason() : string {
return 'privacy:metadata';
}
}

View File

@ -29,3 +29,4 @@ $string['errornotenoughdata'] = 'There is not enough data to evaluate this model
$string['errornotenoughdatadev'] = 'The evaluation results varied too much. It is recommended that more data is gathered to ensure the model is valid. Evaluation results standard deviation = {$a->deviation}, maximum recommended standard deviation = {$a->accepteddeviation}';
$string['errorphp7required'] = 'The PHP machine learning backend requires PHP 7';
$string['pluginname'] = 'PHP machine learning backend';
$string['privacy:metadata'] = 'The PHP machine learning backend plugin does not store any personal data.';

View File

@ -0,0 +1,46 @@
<?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/>.
/**
* Privacy Subsystem implementation for mlbackend_python.
*
* @package mlbackend_python
* @copyright 2018 David Monllao
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mlbackend_python\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for mlbackend_python implementing null_provider.
*
* @copyright 2018 David Monllao
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements \core_privacy\local\metadata\null_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason() : string {
return 'privacy:metadata';
}
}

View File

@ -24,5 +24,6 @@
$string['packageinstalledshouldbe'] = '"moodlemlbackend" python package should be updated. The required version is "{$a->required}" and the installed version is "{$a->installed}"';
$string['pluginname'] = 'Python machine learning backend';
$string['privacy:metadata'] = 'The Python machine learning backend plugin does not store any personal data.';
$string['pythonpackagenotinstalled'] = '"moodlemlbackend" python package is not installed or there is a problem with it. Please execute "{$a}" from command line interface for more info';
$string['pythonpathnotdefined'] = 'The path to your executable Python binary has not been defined. Please visit "{$a}" to set it.';