From 8862838f254398f2fabfd885e2f480667bebafa3 Mon Sep 17 00:00:00 2001 From: Marina Glancy Date: Wed, 16 May 2018 11:03:38 +0800 Subject: [PATCH 1/5] MDL-62147 privacy: corrections to tables, temporary tables --- .../standard/classes/privacy/provider.php | 2 +- .../classes/privacy/provider.php | 62 +++++++++++++++++-- .../lang/en/block_recent_activity.php | 7 +++ lang/en/moodle.php | 10 +++ lang/en/user.php | 8 +++ lib/classes/privacy/provider.php | 10 +++ mod/chat/classes/privacy/provider.php | 22 ++++++- mod/chat/lang/en/chat.php | 9 +++ mod/forum/classes/privacy/provider.php | 8 +++ mod/forum/lang/en/forum.php | 5 ++ user/classes/privacy/provider.php | 14 +++++ 11 files changed, 147 insertions(+), 10 deletions(-) diff --git a/admin/tool/log/store/standard/classes/privacy/provider.php b/admin/tool/log/store/standard/classes/privacy/provider.php index 73ca7671e62..fb82a7508e6 100644 --- a/admin/tool/log/store/standard/classes/privacy/provider.php +++ b/admin/tool/log/store/standard/classes/privacy/provider.php @@ -51,7 +51,7 @@ class provider implements * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $collection) { - $collection->add_database_table('log', [ + $collection->add_database_table('logstore_standard_log', [ 'eventname' => 'privacy:metadata:log:eventname', 'userid' => 'privacy:metadata:log:userid', 'relateduserid' => 'privacy:metadata:log:relateduserid', diff --git a/blocks/recent_activity/classes/privacy/provider.php b/blocks/recent_activity/classes/privacy/provider.php index 240af61bcbd..9a4b9dea265 100644 --- a/blocks/recent_activity/classes/privacy/provider.php +++ b/blocks/recent_activity/classes/privacy/provider.php @@ -25,6 +25,10 @@ namespace block_recent_activity\privacy; +use core_privacy\local\metadata\collection; +use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\contextlist; + defined('MOODLE_INTERNAL') || die(); /** @@ -33,15 +37,61 @@ defined('MOODLE_INTERNAL') || die(); * @copyright 2018 Shamim Rezaie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class provider implements \core_privacy\local\metadata\null_provider { +class provider implements \core_privacy\local\metadata\provider, + \core_privacy\local\request\plugin\provider { /** - * Get the language string identifier with the component's language - * file to explain why this plugin stores no data. + * Returns metadata. * - * @return string + * @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_reason() { - return 'privacy:metadata'; + public static function get_metadata(collection $collection) { + + // This plugin defines a db table but it is not considered personal data and, therefore, not exported or deleted. + $collection->add_database_table('block_recent_activity', [ + 'courseid' => 'privacy:metadata:block_recent_activity:courseid', + 'cmid' => 'privacy:metadata:block_recent_activity:cmid', + 'timecreated' => 'privacy:metadata:block_recent_activity:timecreated', + 'userid' => 'privacy:metadata:block_recent_activity:userid', + 'action' => 'privacy:metadata:block_recent_activity:action', + 'modname' => 'privacy:metadata:block_recent_activity:modname' + ], 'privacy:metadata:block_recent_activity'); + + 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($userid) { + return new 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) { + } + + /** + * 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) { + } + + /** + * 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) { } } diff --git a/blocks/recent_activity/lang/en/block_recent_activity.php b/blocks/recent_activity/lang/en/block_recent_activity.php index 3f6eadb60fa..a569a7a533f 100644 --- a/blocks/recent_activity/lang/en/block_recent_activity.php +++ b/blocks/recent_activity/lang/en/block_recent_activity.php @@ -25,6 +25,13 @@ $string['pluginname'] = 'Recent activity'; $string['privacy:metadata'] = 'The recent activity block contains a cache of data stored elsewhere in Moodle.'; +$string['privacy:metadata:block_recent_activity'] = 'Temporary log of recent teacher activity. Removed after two days'; +$string['privacy:metadata:block_recent_activity:action'] = 'Action: created, updated or deleted'; +$string['privacy:metadata:block_recent_activity:cmid'] = 'Course module id'; +$string['privacy:metadata:block_recent_activity:courseid'] = 'Course id'; +$string['privacy:metadata:block_recent_activity:modname'] = 'Module type name (for delete action)'; +$string['privacy:metadata:block_recent_activity:timecreated'] = 'Time when action was performed'; +$string['privacy:metadata:block_recent_activity:userid'] = 'User performing the action'; $string['recent_activity:addinstance'] = 'Add a new recent activity block'; $string['recent_activity:viewaddupdatemodule'] = 'View added and updated modules in recent activity block'; $string['recent_activity:viewdeletemodule'] = 'View deleted modules in recent activity block'; diff --git a/lang/en/moodle.php b/lang/en/moodle.php index 3a5014c757a..91e53bf5ac2 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -1513,6 +1513,16 @@ $string['privacy:metadata:events_queue:eventdata'] = 'The data stored in the eve $string['privacy:metadata:events_queue:stackdump'] = 'Any stacktrace associated with this event.'; $string['privacy:metadata:events_queue:timecreated'] = 'The time that this event was created.'; $string['privacy:metadata:events_queue:userid'] = 'The userid associated with this event.'; +$string['privacy:metadata:log'] = 'A collection of past events'; +$string['privacy:metadata:log:action'] = 'A description of the action'; +$string['privacy:metadata:log:cmid'] = 'cmid'; +$string['privacy:metadata:log:course'] = 'course'; +$string['privacy:metadata:log:info'] = 'Additional information'; +$string['privacy:metadata:log:ip'] = 'The IP address used at the time of the event'; +$string['privacy:metadata:log:module'] = 'module'; +$string['privacy:metadata:log:time'] = 'The date at wich the action took place'; +$string['privacy:metadata:log:url'] = 'The URL related to the event'; +$string['privacy:metadata:log:userid'] = 'The ID of the user who performed the action'; $string['privacy:metadata:task_adhoc'] = 'The status of adhoc tasks.'; $string['privacy:metadata:task_adhoc:component'] = 'The component owning the task.'; $string['privacy:metadata:task_adhoc:nextruntime'] = 'The earliest time to run this task.'; diff --git a/lang/en/user.php b/lang/en/user.php index 8a4d933a1ff..8b0fd76ba30 100644 --- a/lang/en/user.php +++ b/lang/en/user.php @@ -75,6 +75,10 @@ $string['privacy:metadata:middlename'] = 'The middle name of the user.'; $string['privacy:metadata:mnethostid'] = 'An identifier for the mnet host if used.'; $string['privacy:metadata:model'] = 'The device name, occam or iPhone etc..'; $string['privacy:metadata:msn'] = 'The MSN identifier of the user.'; +$string['privacy:metadata:my_pages'] = 'User pages - dashboard and profile. This table does not contain personal data and only used to link dashboard blocks to users'; +$string['privacy:metadata:my_pages:name'] = 'Page name'; +$string['privacy:metadata:my_pages:private'] = 'Whether or not the page is private (dashboard) or public (profile)'; +$string['privacy:metadata:my_pages:userid'] = 'The user who owns this page or 0 for system defaults'; $string['privacy:metadata:password'] = 'The password for this user to log into the system.'; $string['privacy:metadata:passwordresettablesummary'] = 'A table tracking password reset confirmation tokens'; $string['privacy:metadata:passwordtablesummary'] = 'A rotating log of hashes of previously used passwords for the user.'; @@ -87,6 +91,10 @@ $string['privacy:metadata:reason'] = 'The reason for requesting this course.'; $string['privacy:metadata:requester'] = 'An identifier to a user that requested this course.'; $string['privacy:metadata:requestsummary'] = 'Stores information about requests for courses that users make.'; $string['privacy:metadata:suspended'] = 'A flag to show if the user has been suspended on this system.'; +$string['privacy:metadata:user_preferences'] = 'Preferences associated with the given user'; +$string['privacy:metadata:user_preferences:name'] = 'Preference name'; +$string['privacy:metadata:user_preferences:userid'] = 'User id'; +$string['privacy:metadata:user_preferences:value'] = 'Preference value'; $string['privacy:metadata:username'] = 'The username for this user.'; $string['privacy:metadata:secret'] = 'Secret.. not sure.'; $string['privacy:metadata:sessdata'] = 'Session content'; diff --git a/lib/classes/privacy/provider.php b/lib/classes/privacy/provider.php index 078def9911c..1170f02603b 100644 --- a/lib/classes/privacy/provider.php +++ b/lib/classes/privacy/provider.php @@ -91,6 +91,16 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l 'timecreated' => 'privacy:metadata:events_queue:timecreated', ], 'privacy:metadata:events_queue'); + // The log table is defined in core but used in logstore_legacy. + $collection->add_database_table('log', [ + 'time' => 'privacy:metadata:log:time', + 'userid' => 'privacy:metadata:log:userid', + 'ip' => 'privacy:metadata:log:ip', + 'action' => 'privacy:metadata:log:action', + 'url' => 'privacy:metadata:log:url', + 'info' => 'privacy:metadata:log:info' + ], 'privacy:metadata:log'); + return $collection; } diff --git a/mod/chat/classes/privacy/provider.php b/mod/chat/classes/privacy/provider.php index 40ba43bdc21..ba6c46ab1db 100644 --- a/mod/chat/classes/privacy/provider.php +++ b/mod/chat/classes/privacy/provider.php @@ -65,10 +65,26 @@ class provider implements 'timestamp' => 'privacy:metadata:messages:timestamp', ], 'privacy:metadata:messages'); - // The tables chat_messages_current and chat_users are not reported here + // The tables chat_messages_current and chat_users are not exported/deleted // because they are considered as short-lived data and are deleted on a - // regular basis by cron, or during normal requests. MDL-62006 was raised - // to discuss and/or implement support for those tables. + // regular basis by cron, or during normal requests. TODO MDL-62006. + + $collection->add_database_table('chat_messages_current', [ + 'userid' => 'privacy:metadata:messages:userid', + 'message' => 'privacy:metadata:messages:message', + 'issystem' => 'privacy:metadata:messages:issystem', + 'timestamp' => 'privacy:metadata:messages:timestamp' + ], 'privacy:metadata:chat_messages_current'); + + $collection->add_database_table('chat_users', [ + 'userid' => 'privacy:metadata:chat_users:userid', + 'version' => 'privacy:metadata:chat_users:version', + 'ip' => 'privacy:metadata:chat_users:ip', + 'firstping' => 'privacy:metadata:chat_users:firstping', + 'lastping' => 'privacy:metadata:chat_users:lastping', + 'lastmessageping' => 'privacy:metadata:chat_users:lastmessageping', + 'lang' => 'privacy:metadata:chat_users:lang' + ], 'privacy:metadata:chat_users'); return $collection; } diff --git a/mod/chat/lang/en/chat.php b/mod/chat/lang/en/chat.php index b2ae4df26e7..5d16172855f 100644 --- a/mod/chat/lang/en/chat.php +++ b/mod/chat/lang/en/chat.php @@ -114,6 +114,15 @@ $string['page-mod-chat-x'] = 'Any chat module page'; $string['pastchats'] = 'Past chat sessions'; $string['pluginadministration'] = 'Chat administration'; $string['pluginname'] = 'Chat'; +$string['privacy:metadata:chat_messages_current'] = 'Current chat session. This data is temporary and is deleted after the chat session is deleted'; +$string['privacy:metadata:chat_users'] = 'Keeps track of which users are in which chat rooms'; +$string['privacy:metadata:chat_users:firstping'] = 'Time of the first access to chat room'; +$string['privacy:metadata:chat_users:ip'] = 'User IP'; +$string['privacy:metadata:chat_users:lang'] = 'User language'; +$string['privacy:metadata:chat_users:lastmessageping'] = 'Time of the last message in this chat room'; +$string['privacy:metadata:chat_users:lastping'] = 'Time of the last access to chat room'; +$string['privacy:metadata:chat_users:userid'] = 'User id'; +$string['privacy:metadata:chat_users:version'] = 'How user accessed the chat (sockets/basic/ajax/header_js)'; $string['privacy:metadata:messages'] = 'A record of the messages sent during a chat session'; $string['privacy:metadata:messages:issystem'] = 'Whether the message is a system-generated message'; $string['privacy:metadata:messages:message'] = 'The message'; diff --git a/mod/forum/classes/privacy/provider.php b/mod/forum/classes/privacy/provider.php index 66e99994189..22482fe5d60 100644 --- a/mod/forum/classes/privacy/provider.php +++ b/mod/forum/classes/privacy/provider.php @@ -117,6 +117,14 @@ class provider implements 'forumid' => 'privacy:metadata:forum_track_prefs:forumid', ], 'privacy:metadata:forum_track_prefs'); + // The 'forum_queue' table stores temporary data that is not exported/deleted. + $items->add_database_table('forum_queue', [ + 'userid' => 'privacy:metadata:forum_queue:userid', + 'discussionid' => 'privacy:metadata:forum_queue:discussionid', + 'postid' => 'privacy:metadata:forum_queue:postid', + 'timemodified' => 'privacy:metadata:forum_queue:timemodified' + ], 'privacy:metadata:forum_queue'); + // Forum posts can be tagged and rated. $items->link_subsystem('core_tag', 'privacy:metadata:core_tag'); $items->link_subsystem('core_rating', 'privacy:metadata:core_rating'); diff --git a/mod/forum/lang/en/forum.php b/mod/forum/lang/en/forum.php index 5ccffce5e08..c67ae2741cc 100644 --- a/mod/forum/lang/en/forum.php +++ b/mod/forum/lang/en/forum.php @@ -454,6 +454,11 @@ $string['privacy:metadata:forum_posts:parent'] = 'The parent post that was repli $string['privacy:metadata:forum_posts:subject'] = 'The subject of the forum post.'; $string['privacy:metadata:forum_posts:totalscore'] = 'The message of the forum post.'; $string['privacy:metadata:forum_posts:userid'] = 'The ID of the user who authored the forum post.'; +$string['privacy:metadata:forum_queue'] = 'Temporary log of posts that will be mailed in digest form'; +$string['privacy:metadata:forum_queue:discussionid'] = 'Forum discussion id'; +$string['privacy:metadata:forum_queue:postid'] = 'Forum post id'; +$string['privacy:metadata:forum_queue:timemodified'] = 'The modified time of the original post'; +$string['privacy:metadata:forum_queue:userid'] = 'User who needs to be notified of the post'; $string['privacy:metadata:forum_read'] = 'Information about which posts have been read by the user.'; $string['privacy:metadata:forum_read:discussionid'] = 'The discussion that the post is in.'; $string['privacy:metadata:forum_read:firstread'] = 'The first time that the post was read.'; diff --git a/user/classes/privacy/provider.php b/user/classes/privacy/provider.php index c92732357c0..4b6680e3907 100644 --- a/user/classes/privacy/provider.php +++ b/user/classes/privacy/provider.php @@ -154,6 +154,18 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l 'requester' => 'privacy:metadata:requester' ]; + $mypages = [ + 'userid' => 'privacy:metadata:my_pages:userid', + 'name' => 'privacy:metadata:my_pages:name', + 'private' => 'privacy:metadata:my_pages:private', + ]; + + $userpreferences = [ + 'userid' => 'privacy:metadata:user_preferences:userid', + 'name' => 'privacy:metadata:user_preferences:name', + 'value' => 'privacy:metadata:user_preferences:value' + ]; + $collection->add_database_table('user', $userfields, 'privacy:metadata:usertablesummary'); $collection->add_database_table('user_password_history', $passwordhistory, 'privacy:metadata:passwordtablesummary'); $collection->add_database_table('user_password_resets', $userpasswordresets, 'privacy:metadata:passwordresettablesummary'); @@ -161,6 +173,8 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l $collection->add_database_table('user_devices', $userdevices, 'privacy:metadata:devicetablesummary'); $collection->add_database_table('course_request', $courserequest, 'privacy:metadata:requestsummary'); $collection->add_database_table('sessions', $usersessions, 'privacy:metadata:sessiontablesummary'); + $collection->add_database_table('my_pages', $mypages, 'privacy:metadata:my_pages'); + $collection->add_database_table('user_preferences', $userpreferences, 'privacy:metadata:user_preferences'); $collection->add_subsystem_link('core_files', [], 'privacy:metadata:filelink'); return $collection; From b98339b09a6f88bdda097a7f974e7a679704e752 Mon Sep 17 00:00:00 2001 From: Marina Glancy Date: Wed, 9 May 2018 16:27:07 +0800 Subject: [PATCH 2/5] MDL-62147 privacy: unittest ensures that all tables covered --- privacy/tests/provider_test.php | 73 ++++++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/privacy/tests/provider_test.php b/privacy/tests/provider_test.php index 496d99678b4..0ca9768083b 100644 --- a/privacy/tests/provider_test.php +++ b/privacy/tests/provider_test.php @@ -46,7 +46,10 @@ class provider_testcase extends advanced_testcase { * @return array the array of frankenstyle component names with the relevant class name. */ public function get_component_list() { - $components = []; + $components = ['core' => [ + 'component' => 'core', + 'classname' => manager::get_provider_classname_for_component('core') + ]]; // Get all plugins. $plugintypes = \core_component::get_plugin_types(); foreach ($plugintypes as $plugintype => $typedir) { @@ -204,4 +207,72 @@ class provider_testcase extends advanced_testcase { return false; } + /** + * Finds user fields in a table + * + * Returns fields that have foreign key to user table and fields that are named 'userid'. + * + * @param xmldb_table $table + * @return array + */ + protected function get_userid_fields(xmldb_table $table) { + $userfields = []; + + // Find all fields that have a foreign key to 'id' field in 'user' table. + $keys = $table->getKeys(); + foreach ($keys as $key) { + $reffields = $key->getRefFields(); + $fields = $key->getFields(); + if ($key->getRefTable() === 'user' && count($reffields) == 1 && $reffields[0] == 'id' && count($fields) == 1) { + $userfields[$fields[0]] = $fields[0]; + } + } + // Find fields with the name 'userid' even if they don't have a foreign key. + $fields = $table->getFields(); + foreach ($fields as $field) { + if ($field->getName() == 'userid') { + $userfields['userid'] = 'userid'; + } + } + + return $userfields; + } + + /** + * Test that all tables with user fields are covered by metadata providers + */ + public function test_table_coverage() { + global $DB; + $dbman = $DB->get_manager(); + $schema = $dbman->get_install_xml_schema(); + $tables = []; + foreach ($schema->getTables() as $table) { + if ($table->getName() === 'role_sortorder') { + // TODO MDL-62459 this table is not used anywhere. Remove the table and this statement. + continue; + } + if ($fields = $this->get_userid_fields($table)) { + $tables[$table->getName()] = ' - ' . $table->getName() . ' (' . join(', ', $fields) . ')'; + } + } + + $componentlist = $this->metadata_provider_provider(); + foreach ($componentlist as $componentarray) { + $component = $componentarray['component']; + $classname = $componentarray['classname']; + $collection = new collection($component); + $metadata = $classname::get_metadata($collection); + foreach ($metadata->get_collection() as $item) { + if ($item instanceof database_table) { + unset($tables[$item->get_name()]); + } + } + } + + if ($tables) { + $this->fail("The following tables with user fields must be covered with metadata providers: \n". + join("\n", $tables)); + } + + } } From 876cdf05c4d16759679878d33c71435693ed9d4b Mon Sep 17 00:00:00 2001 From: Marina Glancy Date: Wed, 16 May 2018 10:35:48 +0800 Subject: [PATCH 3/5] MDL-62147 portfolio: add missing tables to privacy provider --- lang/en/portfolio.php | 10 +++ portfolio/classes/privacy/provider.php | 92 ++++++++++++++++++++--- portfolio/tests/privacy_provider_test.php | 26 ++++++- 3 files changed, 117 insertions(+), 11 deletions(-) diff --git a/lang/en/portfolio.php b/lang/en/portfolio.php index 792cb6ec7d6..32d846aa813 100644 --- a/lang/en/portfolio.php +++ b/lang/en/portfolio.php @@ -170,6 +170,16 @@ $string['privacy:metadata'] = 'The portfolio subsystem acts as a channel, passin $string['privacy:metadata:name'] = 'Name of the preference.'; $string['privacy:metadata:instance'] = 'Identifier for the portfolio.'; $string['privacy:metadata:instancesummary'] = 'This stores portfolio both instances and preferences for the portfolios user is using.'; +$string['privacy:metadata:portfolio_log'] = 'Log of portfolio transfers (used to later check for duplicates)'; +$string['privacy:metadata:portfolio_log:caller_class'] = 'Name of the class used to create the transfer'; +$string['privacy:metadata:portfolio_log:caller_component'] = 'Component name responsible for exporting'; +$string['privacy:metadata:portfolio_log:time'] = 'Time of transfer (in the case of a queued transfer this is the time the actual transfer ran, not when the user started)'; +$string['privacy:metadata:portfolio_log:userid'] = 'User who exported content'; +$string['privacy:metadata:portfolio_tempdata'] = 'Stores temporary data for portfolio exports, cleaned by cron after one day'; +$string['privacy:metadata:portfolio_tempdata:data'] = 'Export data'; +$string['privacy:metadata:portfolio_tempdata:expirytime'] = 'Time this record will expire'; +$string['privacy:metadata:portfolio_tempdata:instance'] = 'Portfolio plugin instance being used'; +$string['privacy:metadata:portfolio_tempdata:userid'] = 'User performing export'; $string['privacy:metadata:value'] = 'Value for the preference'; $string['privacy:metadata:userid'] = 'The user Identifier.'; $string['privacy:path'] = 'Portfolio instances'; diff --git a/portfolio/classes/privacy/provider.php b/portfolio/classes/privacy/provider.php index 00e3083ca7e..25bba42d708 100644 --- a/portfolio/classes/privacy/provider.php +++ b/portfolio/classes/privacy/provider.php @@ -29,6 +29,7 @@ use core_privacy\local\metadata\collection; use core_privacy\local\request\context; use core_privacy\local\request\contextlist; use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\transform; /** * Provider for the portfolio API. @@ -56,6 +57,22 @@ class provider implements 'name' => 'privacy:metadata:name', 'value' => 'privacy:metadata:value' ], 'privacy:metadata:instancesummary'); + + $collection->add_database_table('portfolio_log', [ + 'userid' => 'privacy:metadata:portfolio_log:userid', + 'time' => 'privacy:metadata:portfolio_log:time', + 'caller_class' => 'privacy:metadata:portfolio_log:caller_class', + 'caller_component' => 'privacy:metadata:portfolio_log:caller_component', + ], 'privacy:metadata:portfolio_log'); + + // Temporary data is not exported/deleted in privacy API. It is cleaned by cron. + $collection->add_database_table('portfolio_tempdata', [ + 'data' => 'privacy:metadata:portfolio_tempdata:data', + 'expirytime' => 'privacy:metadata:portfolio_tempdata:expirytime', + 'userid' => 'privacy:metadata:portfolio_tempdata:userid', + 'instance' => 'privacy:metadata:portfolio_tempdata:instance', + ], 'privacy:metadata:portfolio_tempdata'); + $collection->add_plugintype_link('portfolio', [], 'privacy:metadata'); return $collection; } @@ -69,9 +86,11 @@ class provider implements public static function get_contexts_for_userid($userid) { $sql = "SELECT ctx.id FROM {context} ctx - JOIN {portfolio_instance_user} piu ON ctx.instanceid = piu.userid AND ctx.contextlevel = :usercontext - WHERE piu.userid = :userid"; - $params = ['userid' => $userid, 'usercontext' => CONTEXT_USER]; + WHERE ctx.instanceid = :userid AND ctx.contextlevel = :usercontext + AND (EXISTS (SELECT 1 FROM {portfolio_instance_user} WHERE userid = :userid1) OR + EXISTS (SELECT 1 FROM {portfolio_log} WHERE userid = :userid2)) + "; + $params = ['userid' => $userid, 'usercontext' => CONTEXT_USER, 'userid1' => $userid, 'userid2' => $userid]; $contextlist = new contextlist(); $contextlist->add_from_sql($sql, $params); return $contextlist; @@ -95,16 +114,63 @@ class provider implements } }); + if (empty($correctusercontext)) { + return; + } + $usercontext = array_shift($correctusercontext); + $sql = "SELECT pi.id AS instanceid, pi.name, + piu.id AS preferenceid, piu.name AS preference, piu.value, + pl.id AS logid, pl.time AS logtime, pl.caller_class, pl.caller_file, + pl.caller_component, pl.returnurl, pl.continueurl + FROM {portfolio_instance} pi + LEFT JOIN {portfolio_instance_user} piu ON piu.instance = pi.id AND piu.userid = :userid1 + LEFT JOIN {portfolio_log} pl ON pl.portfolio = pi.id AND pl.userid = :userid2 + WHERE piu.id IS NOT NULL OR pl.id IS NOT NULL"; + $params = ['userid1' => $usercontext->instanceid, 'userid2' => $usercontext->instanceid]; + $instances = []; + $rs = $DB->get_recordset_sql($sql, $params); + foreach ($rs as $record) { + $instances += [$record->name => + (object)[ + 'name' => $record->name, + 'preferences' => [], + 'logs' => [], + ] + ]; + if ($record->preferenceid) { + $instances[$record->name]->preferences[$record->preferenceid] = (object)[ + 'name' => $record->preference, + 'value' => $record->value, + ]; + } + if ($record->logid) { + $instances[$record->name]->logs[$record->logid] = (object)[ + 'time' => transform::datetime($record->logtime), + 'caller_class' => $record->caller_class, + 'caller_file' => $record->caller_file, + 'caller_component' => $record->caller_component, + 'returnurl' => $record->returnurl, + 'continueurl' => $record->continueurl + ]; + } + } + $rs->close(); - $sql = "SELECT pi.name, piu.name AS preference, piu.value - FROM {portfolio_instance_user} piu - JOIN {portfolio_instance} pi ON piu.instance = pi.id - WHERE piu.userid = :userid"; - $params = ['userid' => $usercontext->instanceid]; - $instances = $DB->get_records_sql($sql, $params); if (!empty($instances)) { + foreach ($instances as &$instance) { + if (!empty($instance->preferences)) { + $instance->preferences = array_values($instance->preferences); + } else { + unset($instance->preferences); + } + if (!empty($instance->logs)) { + $instance->logs = array_values($instance->logs); + } else { + unset($instance->logs); + } + } \core_privacy\local\request\writer::with_context($contextlist->current())->export_data( [get_string('privacy:path', 'portfolio')], (object) $instances); } @@ -120,6 +186,8 @@ class provider implements // Context could be anything, BEWARE! if ($context->contextlevel == CONTEXT_USER) { $DB->delete_records('portfolio_instance_user', ['userid' => $context->instanceid]); + $DB->delete_records('portfolio_tempdata', ['userid' => $context->instanceid]); + $DB->delete_records('portfolio_log', ['userid' => $context->instanceid]); } } @@ -141,9 +209,15 @@ class provider implements } }); + if (empty($correctusercontext)) { + return; + } + $usercontext = array_shift($correctusercontext); $DB->delete_records('portfolio_instance_user', ['userid' => $usercontext->instanceid]); + $DB->delete_records('portfolio_tempdata', ['userid' => $usercontext->instanceid]); + $DB->delete_records('portfolio_log', ['userid' => $usercontext->instanceid]); } /** diff --git a/portfolio/tests/privacy_provider_test.php b/portfolio/tests/privacy_provider_test.php index 93ffb98167c..a1e2a663969 100644 --- a/portfolio/tests/privacy_provider_test.php +++ b/portfolio/tests/privacy_provider_test.php @@ -47,6 +47,22 @@ class portfolio_privacy_provider_test extends \core_privacy\tests\provider_testc 'value' => $value ]; $DB->insert_record('portfolio_instance_user', $userinstance); + + $DB->insert_record('portfolio_log', [ + 'portfolio' => $portfolioinstance->id, + 'userid' => $user->id, + 'caller_class' => 'forum_portfolio_caller', + 'caller_component' => 'mod_forum', + 'time' => time(), + ]); + + $DB->insert_record('portfolio_log', [ + 'portfolio' => $portfolioinstance->id, + 'userid' => $user->id, + 'caller_class' => 'workshop_portfolio_caller', + 'caller_component' => 'mod_workshop', + 'time' => time(), + ]); } /** @@ -57,9 +73,11 @@ class portfolio_privacy_provider_test extends \core_privacy\tests\provider_testc $collection = \core_portfolio\privacy\provider::get_metadata($collection); $this->assertNotEmpty($collection); $items = $collection->get_collection(); - $this->assertEquals(2, count($items)); + $this->assertEquals(4, count($items)); $this->assertInstanceOf(\core_privacy\local\metadata\types\database_table::class, $items[0]); - $this->assertInstanceOf(\core_privacy\local\metadata\types\plugintype_link::class, $items[1]); + $this->assertInstanceOf(\core_privacy\local\metadata\types\database_table::class, $items[1]); + $this->assertInstanceOf(\core_privacy\local\metadata\types\database_table::class, $items[2]); + $this->assertInstanceOf(\core_privacy\local\metadata\types\plugintype_link::class, $items[3]); } /** @@ -105,6 +123,7 @@ class portfolio_privacy_provider_test extends \core_privacy\tests\provider_testc \core_portfolio\privacy\provider::delete_data_for_all_users_in_context($systemcontext); $records = $DB->get_records('portfolio_instance_user'); $this->assertCount(2, $records); + $this->assertCount(4, $DB->get_records('portfolio_log')); $context = context_user::instance($user1->id); \core_portfolio\privacy\provider::delete_data_for_all_users_in_context($context); $records = $DB->get_records('portfolio_instance_user'); @@ -112,6 +131,7 @@ class portfolio_privacy_provider_test extends \core_privacy\tests\provider_testc $this->assertCount(1, $records); $data = array_shift($records); $this->assertEquals($user2->id, $data->userid); + $this->assertCount(2, $DB->get_records('portfolio_log')); } /** @@ -128,6 +148,7 @@ class portfolio_privacy_provider_test extends \core_privacy\tests\provider_testc $records = $DB->get_records('portfolio_instance_user'); $this->assertCount(2, $records); + $this->assertCount(4, $DB->get_records('portfolio_log')); $context = context_user::instance($user1->id); $contextlist = new \core_privacy\local\request\approved_contextlist($user1, 'core_portfolio', [$context->id]); @@ -137,5 +158,6 @@ class portfolio_privacy_provider_test extends \core_privacy\tests\provider_testc $this->assertCount(1, $records); $data = array_shift($records); $this->assertEquals($user2->id, $data->userid); + $this->assertCount(2, $DB->get_records('portfolio_log')); } } From fd982f30308d03216ca87af162298506f5b7e04e Mon Sep 17 00:00:00 2001 From: Marina Glancy Date: Wed, 16 May 2018 10:44:47 +0800 Subject: [PATCH 4/5] MDL-62147 core_grades: Declare metadata of scales and import tables --- grade/classes/privacy/provider.php | 36 +++++++++++++++++++++++++++++- lang/en/grades.php | 25 +++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/grade/classes/privacy/provider.php b/grade/classes/privacy/provider.php index 200fa6713e3..1c3b53e0e96 100644 --- a/grade/classes/privacy/provider.php +++ b/grade/classes/privacy/provider.php @@ -97,9 +97,43 @@ class provider implements 'loggeduser' => 'privacy:metadata:history:loggeduser', ]), 'privacy:metadata:gradeshistory'); - // The table grade_import_values is not reported because its data is temporary and only + $collection->add_database_table('scale_history', [ + 'action' => 'privacy:metadata:scale_history:action', + 'timemodified' => 'privacy:metadata:scale_history:timemodified', + 'loggeduser' => 'privacy:metadata:scale_history:loggeduser', + 'userid' => 'privacy:metadata:scale_history:userid', + 'name' => 'privacy:metadata:scale_history:name', + 'scale' => 'privacy:metadata:scale_history:scale', + 'description' => 'privacy:metadata:scale_history:description' + ], 'privacy:metadata:scale_history'); + + // The following tables are reported but not exported/deleted because their data is temporary and only // used during an import. It's content is deleted after a successful, or failed, import. + $collection->add_database_table('grade_import_newitem', [ + 'itemname' => 'privacy:metadata:grade_import_newitem:itemname', + 'importcode' => 'privacy:metadata:grade_import_newitem:importcode', + 'importer' => 'privacy:metadata:grade_import_newitem:importer' + ], 'privacy:metadata:grade_import_newitem'); + + $collection->add_database_table('grade_import_values', [ + 'userid' => 'privacy:metadata:grade_import_values:userid', + 'finalgrade' => 'privacy:metadata:grade_import_values:finalgrade', + 'feedback' => 'privacy:metadata:grade_import_values:feedback', + 'importcode' => 'privacy:metadata:grade_import_values:importcode', + 'importer' => 'privacy:metadata:grade_import_values:importer', + 'importonlyfeedback' => 'privacy:metadata:grade_import_values:importonlyfeedback' + ], 'privacy:metadata:grade_import_values'); + + // Table 'scale' stores userid of the user who created a scale. This is not considered to be user data. + $collection->add_database_table('scale', [ + 'userid' => 'privacy:metadata:scale:userid', + 'name' => 'privacy:metadata:scale:name', + 'scale' => 'privacy:metadata:scale:scale', + 'timemodified' => 'privacy:metadata:scale:timemodified', + 'description' => 'privacy:metadata:scale:description' + ], 'privacy:metadata:scale'); + return $collection; } diff --git a/lang/en/grades.php b/lang/en/grades.php index b11652e645e..cce740bab4e 100644 --- a/lang/en/grades.php +++ b/lang/en/grades.php @@ -324,6 +324,31 @@ $string['gradereport'] = 'Grade report'; $string['graderreport'] = 'Grader report'; $string['grades'] = 'Grades'; $string['gradesforuser'] = 'Grades for {$a->user}'; +$string['privacy:metadata:grade_import_newitem'] = 'Temporary table for storing new grade_item names from grade import'; +$string['privacy:metadata:grade_import_newitem:importcode'] = 'A unique batch code for identifying one batch of imports'; +$string['privacy:metadata:grade_import_newitem:importer'] = 'User importing the data'; +$string['privacy:metadata:grade_import_newitem:itemname'] = 'New grade item name'; +$string['privacy:metadata:grade_import_values'] = 'Temporary table for importing grades'; +$string['privacy:metadata:grade_import_values:feedback'] = 'Grade feedback'; +$string['privacy:metadata:grade_import_values:finalgrade'] = 'Raw grade value'; +$string['privacy:metadata:grade_import_values:importcode'] = 'A unique batch code for identifying one batch of imports'; +$string['privacy:metadata:grade_import_values:importer'] = 'User importing the data'; +$string['privacy:metadata:grade_import_values:importonlyfeedback'] = 'Flag if only feedback was imported'; +$string['privacy:metadata:grade_import_values:userid'] = 'User whose grade was imported'; +$string['privacy:metadata:scale'] = 'Grading scales, store ID of the user who created the scale. Not considered personal information'; +$string['privacy:metadata:scale:description'] = 'Description of the scale'; +$string['privacy:metadata:scale:name'] = 'Name of the scale'; +$string['privacy:metadata:scale:scale'] = 'Values in the scale'; +$string['privacy:metadata:scale:timemodified'] = 'Time when the scale was last modified'; +$string['privacy:metadata:scale:userid'] = 'ID of the user who created the scale'; +$string['privacy:metadata:scale_history'] = 'History table'; +$string['privacy:metadata:scale_history:action'] = 'created/modified/deleted constants'; +$string['privacy:metadata:scale_history:description'] = 'description'; +$string['privacy:metadata:scale_history:loggeduser'] = 'the userid of the person who last modified this outcome'; +$string['privacy:metadata:scale_history:name'] = 'name'; +$string['privacy:metadata:scale_history:scale'] = 'scale'; +$string['privacy:metadata:scale_history:timemodified'] = 'The last time this grade_item was modified'; +$string['privacy:metadata:scale_history:userid'] = 'userid'; $string['singleview'] = 'Single view for {$a}'; $string['gradesonly'] = 'Change to grades only'; $string['gradesmoduledeletionpendingwarning'] = 'Warning: Activity deletion in progress! Some grades are about to be removed.'; From 06c8d3bf55c7106e4f190beeb0a12a0d15e2a9d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Massart?= Date: Wed, 16 May 2018 12:34:25 +0800 Subject: [PATCH 5/5] MDL-62147 core_grades: Report contexts and data related to scales --- grade/classes/privacy/provider.php | 161 ++++++++++++++++++++++++----- grade/tests/privacy_test.php | 132 ++++++++++++++++++++++- lang/en/grades.php | 40 +++---- 3 files changed, 280 insertions(+), 53 deletions(-) diff --git a/grade/classes/privacy/provider.php b/grade/classes/privacy/provider.php index 1c3b53e0e96..f51b1c0d329 100644 --- a/grade/classes/privacy/provider.php +++ b/grade/classes/privacy/provider.php @@ -60,6 +60,7 @@ class provider implements */ public static function get_metadata(collection $collection) { + // Tables without 'real' user information. $collection->add_database_table('grade_outcomes', [ 'timemodified' => 'privacy:metadata:outcomes:timemodified', 'usermodified' => 'privacy:metadata:outcomes:usermodified', @@ -80,6 +81,18 @@ class provider implements 'loggeduser' => 'privacy:metadata:history:loggeduser', ], 'privacy:metadata:itemshistory'); + $collection->add_database_table('scale', [ + 'userid' => 'privacy:metadata:scale:userid', + 'timemodified' => 'privacy:metadata:scale:timemodified', + ], 'privacy:metadata:scale'); + + $collection->add_database_table('scale_history', [ + 'userid' => 'privacy:metadata:scale:userid', + 'timemodified' => 'privacy:metadata:history:timemodified', + 'loggeduser' => 'privacy:metadata:history:loggeduser', + ], 'privacy:metadata:scalehistory'); + + // Table with user information. $gradescommonfields = [ 'userid' => 'privacy:metadata:grades:userid', 'usermodified' => 'privacy:metadata:grades:usermodified', @@ -97,16 +110,6 @@ class provider implements 'loggeduser' => 'privacy:metadata:history:loggeduser', ]), 'privacy:metadata:gradeshistory'); - $collection->add_database_table('scale_history', [ - 'action' => 'privacy:metadata:scale_history:action', - 'timemodified' => 'privacy:metadata:scale_history:timemodified', - 'loggeduser' => 'privacy:metadata:scale_history:loggeduser', - 'userid' => 'privacy:metadata:scale_history:userid', - 'name' => 'privacy:metadata:scale_history:name', - 'scale' => 'privacy:metadata:scale_history:scale', - 'description' => 'privacy:metadata:scale_history:description' - ], 'privacy:metadata:scale_history'); - // The following tables are reported but not exported/deleted because their data is temporary and only // used during an import. It's content is deleted after a successful, or failed, import. @@ -125,15 +128,6 @@ class provider implements 'importonlyfeedback' => 'privacy:metadata:grade_import_values:importonlyfeedback' ], 'privacy:metadata:grade_import_values'); - // Table 'scale' stores userid of the user who created a scale. This is not considered to be user data. - $collection->add_database_table('scale', [ - 'userid' => 'privacy:metadata:scale:userid', - 'name' => 'privacy:metadata:scale:name', - 'scale' => 'privacy:metadata:scale:scale', - 'timemodified' => 'privacy:metadata:scale:timemodified', - 'description' => 'privacy:metadata:scale:description' - ], 'privacy:metadata:scale'); - return $collection; } @@ -152,18 +146,29 @@ class provider implements FROM {grade_outcomes} go JOIN {context} ctx ON (go.courseid > 0 AND ctx.instanceid = go.courseid AND ctx.contextlevel = :courselevel) - OR (ctx.id = :syscontextid) + OR ((go.courseid IS NULL OR go.courseid < 1) AND ctx.id = :syscontextid) WHERE go.usermodified = :userid"; $params = ['userid' => $userid, 'courselevel' => CONTEXT_COURSE, 'syscontextid' => SYSCONTEXTID]; $contextlist->add_from_sql($sql, $params); - // Add where appear in the history of outcomes, categories or items. + // Add where we modified scales. + $sql = " + SELECT DISTINCT ctx.id + FROM {scale} s + JOIN {context} ctx + ON (s.courseid > 0 AND ctx.instanceid = s.courseid AND ctx.contextlevel = :courselevel) + OR (s.courseid = 0 AND ctx.id = :syscontextid) + WHERE s.userid = :userid"; + $params = ['userid' => $userid, 'courselevel' => CONTEXT_COURSE, 'syscontextid' => SYSCONTEXTID]; + $contextlist->add_from_sql($sql, $params); + + // Add where appear in the history of outcomes, categories, scales or items. $sql = " SELECT DISTINCT ctx.id FROM {context} ctx LEFT JOIN {grade_outcomes_history} goh ON goh.loggeduser = :userid1 AND ( (goh.courseid > 0 AND goh.courseid = ctx.instanceid AND ctx.contextlevel = :courselevel1) - OR ((goh.courseid IS NULL OR goh.courseid < 1) AND ctx.id = :syscontextid) + OR ((goh.courseid IS NULL OR goh.courseid < 1) AND ctx.id = :syscontextid1) ) LEFT JOIN {grade_categories_history} gch ON gch.loggeduser = :userid2 AND ( gch.courseid = ctx.instanceid @@ -173,17 +178,28 @@ class provider implements gih.courseid = ctx.instanceid AND ctx.contextlevel = :courselevel3 ) + LEFT JOIN {scale_history} sh + ON (sh.userid = :userid4 OR sh.loggeduser = :userid5) + AND ( + (sh.courseid > 0 AND sh.courseid = ctx.instanceid AND ctx.contextlevel = :courselevel4) + OR (sh.courseid = 0 AND ctx.id = :syscontextid2) + ) WHERE goh.id IS NOT NULL OR gch.id IS NOT NULL - OR gih.id IS NOT NULL"; + OR gih.id IS NOT NULL + OR sh.id IS NOT NULL"; $params = [ - 'syscontextid' => SYSCONTEXTID, + 'syscontextid1' => SYSCONTEXTID, + 'syscontextid2' => SYSCONTEXTID, 'courselevel1' => CONTEXT_COURSE, 'courselevel2' => CONTEXT_COURSE, 'courselevel3' => CONTEXT_COURSE, + 'courselevel4' => CONTEXT_COURSE, 'userid1' => $userid, 'userid2' => $userid, 'userid3' => $userid, + 'userid4' => $userid, + 'userid5' => $userid, ]; $contextlist->add_from_sql($sql, $params); @@ -274,6 +290,9 @@ class provider implements // Export the outcomes. static::export_user_data_outcomes_in_contexts($contextlist); + // Export the scales. + static::export_user_data_scales_in_contexts($contextlist); + // Export the historical grades which have become orphans (their grade items were deleted). // We place those in ther user context of the graded user. $userids = array_values(array_map(function($context) { @@ -693,6 +712,100 @@ class provider implements }); } + /** + * Export the user data related to scales. + * + * @param approved_contextlist $contextlist The approved contexts to export information for. + * @return void + */ + protected static function export_user_data_scales_in_contexts(approved_contextlist $contextlist) { + global $DB; + + $rootpath = [get_string('grades', 'core_grades')]; + $relatedtomepath = array_merge($rootpath, [get_string('privacy:path:relatedtome', 'core_grades')]); + $userid = $contextlist->get_user()->id; + + // Reorganise the contexts. + $reduced = array_reduce($contextlist->get_contexts(), function($carry, $context) { + if ($context->contextlevel == CONTEXT_SYSTEM) { + $carry['in_system'] = true; + } else if ($context->contextlevel == CONTEXT_COURSE) { + $carry['courseids'][] = $context->instanceid; + } + return $carry; + }, [ + 'in_system' => false, + 'courseids' => [] + ]); + + // Construct SQL. + $sqltemplateparts = []; + $templateparams = []; + if ($reduced['in_system']) { + $sqltemplateparts[] = '{prefix}.courseid = 0'; + } + if (!empty($reduced['courseids'])) { + list($insql, $inparams) = $DB->get_in_or_equal($reduced['courseids'], SQL_PARAMS_NAMED); + $sqltemplateparts[] = "{prefix}.courseid $insql"; + $templateparams = array_merge($templateparams, $inparams); + } + if (empty($sqltemplateparts)) { + return; + } + $sqltemplate = '(' . implode(' OR ', $sqltemplateparts) . ')'; + + // Export edited scales. + $sqlwhere = str_replace('{prefix}', 's', $sqltemplate); + $sql = " + SELECT s.id, s.courseid, s.name, s.timemodified + FROM {scale} s + WHERE $sqlwhere + AND s.userid = :userid + ORDER BY s.courseid, s.timemodified, s.id"; + $params = array_merge($templateparams, ['userid' => $userid]); + $recordset = $DB->get_recordset_sql($sql, $params); + static::recordset_loop_and_export($recordset, 'courseid', [], function($carry, $record) { + $carry[] = [ + 'name' => $record->name, + 'timemodified' => transform::datetime($record->timemodified), + 'created_or_modified_by_you' => transform::yesno(true) + ]; + return $carry; + + }, function($courseid, $data) use ($relatedtomepath) { + $context = $courseid ? context_course::instance($courseid) : context_system::instance(); + writer::with_context($context)->export_related_data($relatedtomepath, 'scales', + (object) ['scales' => $data]); + }); + + // Export edits of scales history. + $sqlwhere = str_replace('{prefix}', 'sh', $sqltemplate); + $sql = " + SELECT sh.id, sh.courseid, sh.name, sh.userid, sh.timemodified, sh.action, sh.loggeduser + FROM {scale_history} sh + WHERE $sqlwhere + AND sh.loggeduser = :userid1 + OR sh.userid = :userid2 + ORDER BY sh.courseid, sh.timemodified, sh.id"; + $params = array_merge($templateparams, ['userid1' => $userid, 'userid2' => $userid]); + $recordset = $DB->get_recordset_sql($sql, $params); + static::recordset_loop_and_export($recordset, 'courseid', [], function($carry, $record) use ($userid) { + $carry[] = [ + 'name' => $record->name, + 'timemodified' => transform::datetime($record->timemodified), + 'author_of_change_was_you' => transform::yesno($record->userid == $userid), + 'author_of_action_was_you' => transform::yesno($record->loggeduser == $userid), + 'action' => static::transform_history_action($record->action) + ]; + return $carry; + + }, function($courseid, $data) use ($relatedtomepath) { + $context = $courseid ? context_course::instance($courseid) : context_system::instance(); + writer::with_context($context)->export_related_data($relatedtomepath, 'scales_history', + (object) ['modified_records' => $data]); + }); + } + /** * Extract grade_grade from a record. * diff --git a/grade/tests/privacy_test.php b/grade/tests/privacy_test.php index 83605a11c00..f7dfc15c569 100644 --- a/grade/tests/privacy_test.php +++ b/grade/tests/privacy_test.php @@ -64,6 +64,11 @@ class core_grades_privacy_testcase extends provider_testcase { $u4 = $dg->create_user(); $u5 = $dg->create_user(); $u6 = $dg->create_user(); + $u7 = $dg->create_user(); + $u8 = $dg->create_user(); + $u9 = $dg->create_user(); + $u10 = $dg->create_user(); + $u11 = $dg->create_user(); $sysctx = context_system::instance(); $c1ctx = context_course::instance($c1->id); @@ -80,16 +85,22 @@ class core_grades_privacy_testcase extends provider_testcase { 'fullname' => 'go2']), false); // Nothing as of now. - foreach ([$u1, $u2, $u3, $u4] as $u) { + foreach ([$u1, $u2, $u3, $u4, $u5, $u6, $u7, $u8, $u9, $u10, $u11] as $u) { $contexts = array_flip(provider::get_contexts_for_userid($u->id)->get_contextids()); $this->assertEmpty($contexts); } $go0 = new grade_outcome(['shortname' => 'go0', 'fullname' => 'go0', 'usermodified' => $u1->id]); $go0->insert(); - $go1 = new grade_outcome(['shortname' => 'go1', 'fullname' => 'go1', 'courseid' => $c1->id, 'usermodified' => $u1->id]); + $go1 = new grade_outcome(['shortname' => 'go1', 'fullname' => 'go1', 'courseid' => $c1->id, 'usermodified' => $u11->id]); $go1->insert(); + // Create scales. + $s1 = new grade_scale(['name' => 's1', 'scale' => 'a,b', 'userid' => $u7->id, 'courseid' => 0, 'description' => '']); + $s1->insert(); + $s2 = new grade_scale(['name' => 's2', 'scale' => 'a,b', 'userid' => $u8->id, 'courseid' => $c1->id, 'description' => '']); + $s2->insert(); + // User 2 creates history. $this->setUser($u2); $go0->shortname .= ' edited'; @@ -118,11 +129,18 @@ class core_grades_privacy_testcase extends provider_testcase { $this->setUser($u6); $gi2a->delete(); + // User 9 creates history. + $this->setUser($u9); + $s1->name .= ' edited'; + $s1->update(); + // Assert contexts. $contexts = array_flip(provider::get_contexts_for_userid($u1->id)->get_contextids()); - $this->assertCount(2, $contexts); - $this->assertArrayHasKey($c1ctx->id, $contexts); + $this->assertCount(1, $contexts); $this->assertArrayHasKey($sysctx->id, $contexts); + $contexts = array_flip(provider::get_contexts_for_userid($u11->id)->get_contextids()); + $this->assertCount(1, $contexts); + $this->assertArrayHasKey($c1ctx->id, $contexts); $contexts = array_flip(provider::get_contexts_for_userid($u2->id)->get_contextids()); $this->assertCount(2, $contexts); $this->assertArrayHasKey($sysctx->id, $contexts); @@ -140,6 +158,23 @@ class core_grades_privacy_testcase extends provider_testcase { $contexts = array_flip(provider::get_contexts_for_userid($u6->id)->get_contextids()); $this->assertCount(1, $contexts); $this->assertArrayHasKey($c2ctx->id, $contexts); + $contexts = array_flip(provider::get_contexts_for_userid($u7->id)->get_contextids()); + $this->assertCount(1, $contexts); + $this->assertArrayHasKey($sysctx->id, $contexts); + $contexts = array_flip(provider::get_contexts_for_userid($u8->id)->get_contextids()); + $this->assertCount(1, $contexts); + $this->assertArrayHasKey($c1ctx->id, $contexts); + $contexts = array_flip(provider::get_contexts_for_userid($u9->id)->get_contextids()); + $this->assertCount(1, $contexts); + $this->assertArrayHasKey($sysctx->id, $contexts); + + // User 10 creates history. + $this->setUser($u10); + $s2->delete(); + + $contexts = array_flip(provider::get_contexts_for_userid($u10->id)->get_contextids()); + $this->assertCount(1, $contexts); + $this->assertArrayHasKey($c1ctx->id, $contexts); } public function test_get_contexts_for_userid_grades_and_history() { @@ -609,6 +644,10 @@ class core_grades_privacy_testcase extends provider_testcase { $u4 = $dg->create_user(); $u5 = $dg->create_user(); $u6 = $dg->create_user(); + $u7 = $dg->create_user(); + $u8 = $dg->create_user(); + $u9 = $dg->create_user(); + $u10 = $dg->create_user(); $sysctx = context_system::instance(); $u1ctx = context_user::instance($u1->id); @@ -641,6 +680,14 @@ class core_grades_privacy_testcase extends provider_testcase { $go1 = new grade_outcome(['shortname' => 'go1', 'fullname' => 'go1', 'courseid' => $c1->id, 'usermodified' => $u1->id]); $go1->insert(); + // Create scales. + $s1 = new grade_scale(['name' => 's1', 'scale' => 'a,b', 'userid' => $u7->id, 'courseid' => 0, 'description' => '']); + $s1->insert(); + $s2 = new grade_scale(['name' => 's2', 'scale' => 'a,b', 'userid' => $u8->id, 'courseid' => $c1->id, 'description' => '']); + $s2->insert(); + $s3 = new grade_scale(['name' => 's3', 'scale' => 'a,b', 'userid' => $u8->id, 'courseid' => $c2->id, 'description' => '']); + $s3->insert(); + // User 2 creates history. $this->setUser($u2); $go0->shortname .= ' edited'; @@ -669,6 +716,15 @@ class core_grades_privacy_testcase extends provider_testcase { $this->setUser($u6); $gi2a->delete(); + // User 9 creates history. + $this->setUser($u9); + $s1->name .= ' edited'; + $s1->update(); + + // User 10 creates history. + $this->setUser($u10); + $s3->delete(); + $this->setAdminUser(); // Export data for u1. @@ -755,6 +811,74 @@ class core_grades_privacy_testcase extends provider_testcase { $this->assertEquals(transform::yesno(true), $data->modified_records[0]['logged_in_user_was_you']); $this->assertEquals(get_string('privacy:request:historyactiondelete', 'core_grades'), $data->modified_records[0]['action']); + + // Export data for u7. + writer::reset(); + provider::export_user_data(new approved_contextlist($u7, 'core_grades', $allcontexts)); + $data = writer::with_context($c1ctx)->get_related_data($relatedtomepath, 'scales'); + $this->assertEmpty($data); + $data = writer::with_context($sysctx)->get_related_data($relatedtomepath, 'scales'); + $this->assertCount(1, $data->scales); + $this->assertEquals($s1->name, $data->scales[0]['name']); + $this->assertEquals(transform::yesno(true), $data->scales[0]['created_or_modified_by_you']); + + // Export data for u8. + writer::reset(); + provider::export_user_data(new approved_contextlist($u8, 'core_grades', $allcontexts)); + $data = writer::with_context($sysctx)->get_related_data($relatedtomepath, 'scales'); + $this->assertEmpty($data); + $data = writer::with_context($c1ctx)->get_related_data($relatedtomepath, 'scales'); + $this->assertCount(1, $data->scales); + $this->assertEquals($s2->name, $data->scales[0]['name']); + $this->assertEquals(transform::yesno(true), $data->scales[0]['created_or_modified_by_you']); + $data = writer::with_context($c2ctx)->get_related_data($relatedtomepath, 'scales_history'); + $this->assertCount(2, $data->modified_records); + $this->assertEquals($s3->name, $data->modified_records[0]['name']); + $this->assertEquals(transform::yesno(true), $data->modified_records[0]['author_of_change_was_you']); + $this->assertEquals(transform::yesno(false), $data->modified_records[0]['author_of_action_was_you']); + $this->assertEquals(get_string('privacy:request:historyactioninsert', 'core_grades'), + $data->modified_records[0]['action']); + $this->assertEquals($s3->name, $data->modified_records[1]['name']); + $this->assertEquals(transform::yesno(true), $data->modified_records[1]['author_of_change_was_you']); + $this->assertEquals(transform::yesno(false), $data->modified_records[1]['author_of_action_was_you']); + $this->assertEquals(get_string('privacy:request:historyactiondelete', 'core_grades'), + $data->modified_records[1]['action']); + + // Export data for u9. + writer::reset(); + provider::export_user_data(new approved_contextlist($u9, 'core_grades', $allcontexts)); + $data = writer::with_context($c1ctx)->get_related_data($relatedtomepath, 'scales'); + $this->assertEmpty($data); + $data = writer::with_context($sysctx)->get_related_data($relatedtomepath, 'scales'); + $this->assertEmpty($data); + $data = writer::with_context($sysctx)->get_related_data($relatedtomepath, 'scales_history'); + $this->assertCount(1, $data->modified_records); + $this->assertEquals($s1->name, $data->modified_records[0]['name']); + $this->assertEquals(transform::yesno(false), $data->modified_records[0]['author_of_change_was_you']); + $this->assertEquals(transform::yesno(true), $data->modified_records[0]['author_of_action_was_you']); + $this->assertEquals(get_string('privacy:request:historyactionupdate', 'core_grades'), + $data->modified_records[0]['action']); + + // Export data for u10. + writer::reset(); + provider::export_user_data(new approved_contextlist($u10, 'core_grades', $allcontexts)); + $data = writer::with_context($c1ctx)->get_related_data($relatedtomepath, 'scales'); + $this->assertEmpty($data); + $data = writer::with_context($c2ctx)->get_related_data($relatedtomepath, 'scales'); + $this->assertEmpty($data); + $data = writer::with_context($sysctx)->get_related_data($relatedtomepath, 'scales'); + $this->assertEmpty($data); + $data = writer::with_context($c1ctx)->get_related_data($relatedtomepath, 'scales_history'); + $this->assertEmpty($data); + $data = writer::with_context($sysctx)->get_related_data($relatedtomepath, 'scales_history'); + $this->assertEmpty($data); + $data = writer::with_context($c2ctx)->get_related_data($relatedtomepath, 'scales_history'); + $this->assertCount(1, $data->modified_records); + $this->assertEquals($s3->name, $data->modified_records[0]['name']); + $this->assertEquals(transform::yesno(false), $data->modified_records[0]['author_of_change_was_you']); + $this->assertEquals(transform::yesno(true), $data->modified_records[0]['author_of_action_was_you']); + $this->assertEquals(get_string('privacy:request:historyactiondelete', 'core_grades'), + $data->modified_records[0]['action']); } /** diff --git a/lang/en/grades.php b/lang/en/grades.php index cce740bab4e..2153ef4855b 100644 --- a/lang/en/grades.php +++ b/lang/en/grades.php @@ -324,31 +324,6 @@ $string['gradereport'] = 'Grade report'; $string['graderreport'] = 'Grader report'; $string['grades'] = 'Grades'; $string['gradesforuser'] = 'Grades for {$a->user}'; -$string['privacy:metadata:grade_import_newitem'] = 'Temporary table for storing new grade_item names from grade import'; -$string['privacy:metadata:grade_import_newitem:importcode'] = 'A unique batch code for identifying one batch of imports'; -$string['privacy:metadata:grade_import_newitem:importer'] = 'User importing the data'; -$string['privacy:metadata:grade_import_newitem:itemname'] = 'New grade item name'; -$string['privacy:metadata:grade_import_values'] = 'Temporary table for importing grades'; -$string['privacy:metadata:grade_import_values:feedback'] = 'Grade feedback'; -$string['privacy:metadata:grade_import_values:finalgrade'] = 'Raw grade value'; -$string['privacy:metadata:grade_import_values:importcode'] = 'A unique batch code for identifying one batch of imports'; -$string['privacy:metadata:grade_import_values:importer'] = 'User importing the data'; -$string['privacy:metadata:grade_import_values:importonlyfeedback'] = 'Flag if only feedback was imported'; -$string['privacy:metadata:grade_import_values:userid'] = 'User whose grade was imported'; -$string['privacy:metadata:scale'] = 'Grading scales, store ID of the user who created the scale. Not considered personal information'; -$string['privacy:metadata:scale:description'] = 'Description of the scale'; -$string['privacy:metadata:scale:name'] = 'Name of the scale'; -$string['privacy:metadata:scale:scale'] = 'Values in the scale'; -$string['privacy:metadata:scale:timemodified'] = 'Time when the scale was last modified'; -$string['privacy:metadata:scale:userid'] = 'ID of the user who created the scale'; -$string['privacy:metadata:scale_history'] = 'History table'; -$string['privacy:metadata:scale_history:action'] = 'created/modified/deleted constants'; -$string['privacy:metadata:scale_history:description'] = 'description'; -$string['privacy:metadata:scale_history:loggeduser'] = 'the userid of the person who last modified this outcome'; -$string['privacy:metadata:scale_history:name'] = 'name'; -$string['privacy:metadata:scale_history:scale'] = 'scale'; -$string['privacy:metadata:scale_history:timemodified'] = 'The last time this grade_item was modified'; -$string['privacy:metadata:scale_history:userid'] = 'userid'; $string['singleview'] = 'Single view for {$a}'; $string['gradesonly'] = 'Change to grades only'; $string['gradesmoduledeletionpendingwarning'] = 'Warning: Activity deletion in progress! Some grades are about to be removed.'; @@ -633,6 +608,17 @@ $string['prefrows'] = 'Special rows'; $string['prefshow'] = 'Show/hide toggles'; $string['previewrows'] = 'Preview rows'; $string['privacy:metadata:categorieshistory'] = 'A record of previous versions of grade categories'; +$string['privacy:metadata:grade_import_newitem'] = 'Temporary table for storing new grade_item names from grade import'; +$string['privacy:metadata:grade_import_newitem:importcode'] = 'A unique batch code for identifying one batch of imports'; +$string['privacy:metadata:grade_import_newitem:importer'] = 'User importing the data'; +$string['privacy:metadata:grade_import_newitem:itemname'] = 'New grade item name'; +$string['privacy:metadata:grade_import_values'] = 'Temporary table for importing grades'; +$string['privacy:metadata:grade_import_values:feedback'] = 'Grade feedback'; +$string['privacy:metadata:grade_import_values:finalgrade'] = 'Raw grade value'; +$string['privacy:metadata:grade_import_values:importcode'] = 'A unique batch code for identifying one batch of imports'; +$string['privacy:metadata:grade_import_values:importer'] = 'User importing the data'; +$string['privacy:metadata:grade_import_values:importonlyfeedback'] = 'Flag if only feedback was imported'; +$string['privacy:metadata:grade_import_values:userid'] = 'User whose grade was imported'; $string['privacy:metadata:grades'] = 'A record of grades'; $string['privacy:metadata:grades:aggregationstatus'] = 'The aggregation status'; $string['privacy:metadata:grades:aggregationweight'] = 'The weight in aggregation'; @@ -650,6 +636,10 @@ $string['privacy:metadata:outcomes'] = 'A record of outcomes'; $string['privacy:metadata:outcomes:timemodified'] = 'Time at which the record was modified'; $string['privacy:metadata:outcomes:usermodified'] = 'The user who last modified the record'; $string['privacy:metadata:outcomeshistory'] = 'A record of previous versions of outcomes'; +$string['privacy:metadata:scale'] = 'A record of scales'; +$string['privacy:metadata:scale:timemodified'] = 'Time at which the record was last modified'; +$string['privacy:metadata:scale:userid'] = 'The user who last modified the record'; +$string['privacy:metadata:scalehistory'] = 'A record of previous versions of scales'; $string['privacy:path:relatedtome'] = 'Related to me'; $string['privacy:request:historyactiondelete'] = 'Delete'; $string['privacy:request:historyactioninsert'] = 'Insert';