diff --git a/mod/data/classes/privacy/provider.php b/mod/data/classes/privacy/provider.php index b2c36a8c3a3..10b6d7b531e 100644 --- a/mod/data/classes/privacy/provider.php +++ b/mod/data/classes/privacy/provider.php @@ -26,9 +26,11 @@ namespace mod_data\privacy; use core_privacy\local\metadata\collection; use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\approved_userlist; use core_privacy\local\request\contextlist; use core_privacy\local\request\helper; use core_privacy\local\request\transform; +use core_privacy\local\request\userlist; use core_privacy\local\request\writer; use core_privacy\manager; @@ -45,6 +47,9 @@ class provider implements // This plugin stores personal data. \core_privacy\local\metadata\provider, + // This plugin is capable of determining which users have data within it. + \core_privacy\local\request\core_userlist_provider, + // This plugin is a core_user_data_provider. \core_privacy\local\request\plugin\provider { @@ -126,6 +131,72 @@ class provider implements return $contextlist; } + /** + * Get the list of users who have data within a context. + * + * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. + * + */ + public static function get_users_in_context(userlist $userlist) { + $context = $userlist->get_context(); + + if (!is_a($context, \context_module::class)) { + return; + } + + // Find users with data records. + $sql = "SELECT dr.userid + FROM {context} c + JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel + JOIN {modules} m ON m.id = cm.module AND m.name = :modname + JOIN {data} d ON d.id = cm.instance + JOIN {data_records} dr ON dr.dataid = d.id + WHERE c.id = :contextid"; + + $params = [ + 'modname' => 'data', + 'contextid' => $context->id, + 'contextlevel' => CONTEXT_MODULE, + ]; + + $userlist->add_from_sql('userid', $sql, $params); + + // Find users with comments. + $sql = "SELECT dr.id + FROM {context} c + JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel + JOIN {modules} m ON m.id = cm.module AND m.name = :modname + JOIN {data} d ON d.id = cm.instance + JOIN {data_records} dr ON dr.dataid = d.id + WHERE c.id = :contextid"; + + $params = [ + 'modname' => 'data', + 'contextid' => $context->id, + 'contextlevel' => CONTEXT_MODULE, + ]; + + \core_comment\privacy\provider::get_users_in_context_from_sql( + $userlist, 'com', 'mod_data', 'database_entry', $sql, $params); + + // Find users with ratings. + $sql = "SELECT dr.id + FROM {context} c + JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel + JOIN {modules} m ON m.id = cm.module AND m.name = :modname + JOIN {data} d ON d.id = cm.instance + JOIN {data_records} dr ON dr.dataid = d.id + WHERE c.id = :contextid"; + + $params = [ + 'modname' => 'data', + 'contextid' => $context->id, + 'contextlevel' => CONTEXT_MODULE, + ]; + + \core_rating\privacy\provider::get_users_in_context_from_sql($userlist, 'rat', 'mod_data', 'entry', $sql, $params); + } + /** * Creates an object from all fields in the $record where key starts with $prefix * @@ -385,6 +456,51 @@ class provider implements // We do not delete ratings made by this user on other records because it may change grades. } + /** + * Delete multiple users within a single context. + * + * @param approved_userlist $userlist The approved context and user information to delete information for. + */ + public static function delete_data_for_users(approved_userlist $userlist) { + global $DB; + + $context = $userlist->get_context(); + $recordstobedeleted = []; + list($userinsql, $userinparams) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED); + + $sql = "SELECT " . self::sql_fields() . " + FROM {context} ctx + JOIN {course_modules} cm ON cm.id = ctx.instanceid + JOIN {modules} m ON m.id = cm.module AND m.name = :modname + JOIN {data} d ON d.id = cm.instance + JOIN {data_records} dr ON dr.dataid = d.id AND dr.userid {$userinsql} + LEFT JOIN {data_content} dc ON dc.recordid = dr.id + LEFT JOIN {data_fields} df ON df.id = dc.fieldid + WHERE ctx.id = :ctxid AND ctx.contextlevel = :contextlevel + ORDER BY dr.id"; + + $params = [ + 'ctxid' => $context->id, + 'contextlevel' => CONTEXT_MODULE, + 'modname' => 'data', + ]; + $params += $userinparams; + + $rs = $DB->get_recordset_sql($sql, $params); + foreach ($rs as $row) { + self::mark_data_content_for_deletion($context, $row); + $recordstobedeleted[$row->recordid] = $row->recordid; + } + $rs->close(); + + self::delete_data_records($context, $recordstobedeleted); + + // Additionally remove comments these users made on other entries. + \core_comment\privacy\provider::delete_comments_for_users($userlist, 'mod_data', 'database_entry'); + + // We do not delete ratings made by users on other records because it may change grades. + } + /** * Marks a data_record/data_content for deletion * diff --git a/mod/data/tests/privacy_provider_test.php b/mod/data/tests/privacy_provider_test.php index c85c9947710..31d7c7bee80 100644 --- a/mod/data/tests/privacy_provider_test.php +++ b/mod/data/tests/privacy_provider_test.php @@ -23,6 +23,7 @@ */ use core_privacy\local\metadata\collection; +use core_privacy\local\request\approved_userlist; use mod_data\privacy\provider; defined('MOODLE_INTERNAL') || die(); @@ -39,6 +40,8 @@ class mod_data_privacy_provider_testcase extends \core_privacy\tests\provider_te protected $student; /** @var stdClass The student object. */ protected $student2; + /** @var stdClass The student object. */ + protected $student3; /** @var stdClass The data object. */ protected $datamodule; @@ -84,9 +87,11 @@ class mod_data_privacy_provider_testcase extends \core_privacy\tests\provider_te // Create a student. $student1 = $generator->create_user(); $student2 = $generator->create_user(); + $student3 = $generator->create_user(); $studentrole = $DB->get_record('role', ['shortname' => 'student']); $generator->enrol_user($student1->id, $course->id, $studentrole->id); $generator->enrol_user($student2->id, $course->id, $studentrole->id); + $generator->enrol_user($student3->id, $course->id, $studentrole->id); // Add records. $this->setUser($student1); @@ -98,8 +103,12 @@ class mod_data_privacy_provider_testcase extends \core_privacy\tests\provider_te $this->generate_data_record($datamodule); $this->generate_data_record($datamodule); + $this->setUser($student3); + $this->generate_data_record($datamodule); + $this->student = $student1; $this->student2 = $student2; + $this->student3 = $student3; $this->datamodule = $datamodule; $this->course = $course; } @@ -177,6 +186,27 @@ class mod_data_privacy_provider_testcase extends \core_privacy\tests\provider_te $this->assertEquals($cmcontext->id, $contextforuser->id); } + /** + * Test for provider::get_users_in_context(). + */ + public function test_get_users_in_context() { + $component = 'mod_data'; + $cm = get_coursemodule_from_instance('data', $this->datamodule->id); + $cmcontext = context_module::instance($cm->id); + + $userlist = new \core_privacy\local\request\userlist($cmcontext, $component); + provider::get_users_in_context($userlist); + + $this->assertCount(3, $userlist); + + $expected = [$this->student->id, $this->student2->id, $this->student3->id]; + $actual = $userlist->get_userids(); + sort($expected); + sort($actual); + + $this->assertEquals($expected, $actual); + } + /** * Get test privacy writer * @@ -240,4 +270,46 @@ class mod_data_privacy_provider_testcase extends \core_privacy\tests\provider_te provider::export_user_data($appctxt); $this->assertFalse($this->get_writer($cmcontext)->has_any_data()); } + + /** + * Test for provider::delete_data_for_users(). + */ + public function test_delete_data_for_users() { + $cm = get_coursemodule_from_instance('data', $this->datamodule->id); + $cmcontext = context_module::instance($cm->id); + $userstodelete = [$this->student->id, $this->student2->id]; + + // Ensure student, student 2 and student 3 have data before being deleted. + $appctxt = new \core_privacy\local\request\approved_contextlist($this->student, 'mod_data', [$cmcontext->id]); + provider::export_user_data($appctxt); + $this->assertTrue($this->get_writer($cmcontext)->has_any_data()); + + $appctxt = new \core_privacy\local\request\approved_contextlist($this->student2, 'mod_data', [$cmcontext->id]); + provider::export_user_data($appctxt); + $this->assertTrue($this->get_writer($cmcontext)->has_any_data()); + + // Delete data for student 1 and 2. + $approvedlist = new approved_userlist($cmcontext, 'mod_data', $userstodelete); + provider::delete_data_for_users($approvedlist); + + // Reset the writer so it doesn't contain the data from before deletion. + \core_privacy\local\request\writer::reset(); + + // Ensure data is now deleted for student and student 2. + $appctxt = new \core_privacy\local\request\approved_contextlist($this->student, 'mod_data', [$cmcontext->id]); + provider::export_user_data($appctxt); + + $this->assertFalse($this->get_writer($cmcontext)->has_any_data()); + + $appctxt = new \core_privacy\local\request\approved_contextlist($this->student2, 'mod_data', [$cmcontext->id]); + provider::export_user_data($appctxt); + + $this->assertFalse($this->get_writer($cmcontext)->has_any_data()); + + // Ensure data still intact for student 3. + $appctxt = new \core_privacy\local\request\approved_contextlist($this->student3, 'mod_data', [$cmcontext->id]); + provider::export_user_data($appctxt); + + $this->assertTrue($this->get_writer($cmcontext)->has_any_data()); + } }