From 55d1ef377ca390bebebf528c77bddc4c06ffe483 Mon Sep 17 00:00:00 2001 From: Michael Hawkins Date: Fri, 21 Sep 2018 15:05:10 +0800 Subject: [PATCH] MDL-63497 mod_glossary: Add support for removal of context users This issue is a part of the MDL-62560 Epic. Also added missing ratings include and test to mod_glossary unit tests. --- mod/glossary/classes/privacy/provider.php | 125 ++++++++++++++++ mod/glossary/tests/privacy_provider_test.php | 147 ++++++++++++++++++- 2 files changed, 271 insertions(+), 1 deletion(-) diff --git a/mod/glossary/classes/privacy/provider.php b/mod/glossary/classes/privacy/provider.php index 691fe687a2c45..4c630f6537504 100644 --- a/mod/glossary/classes/privacy/provider.php +++ b/mod/glossary/classes/privacy/provider.php @@ -24,9 +24,11 @@ namespace mod_glossary\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\deletion_criteria; use core_privacy\local\request\helper; +use core_privacy\local\request\userlist; use core_privacy\local\request\writer; defined('MOODLE_INTERNAL') || die(); @@ -39,6 +41,8 @@ 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 { @@ -101,6 +105,72 @@ public static function get_contexts_for_userid(int $userid) : contextlist { 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 glossary entries. + $sql = "SELECT ge.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 {glossary} g ON g.id = cm.instance + JOIN {glossary_entries} ge ON ge.glossaryid = g.id + WHERE c.id = :contextid"; + + $params = [ + 'contextid' => $context->id, + 'contextlevel' => CONTEXT_MODULE, + 'modname' => 'glossary', + ]; + + $userlist->add_from_sql('userid', $sql, $params); + + // Find users with glossary comments. + $sql = "SELECT ge.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 {glossary} g ON g.id = cm.instance + JOIN {glossary_entries} ge ON ge.glossaryid = g.id + WHERE c.id = :contextid"; + + $params = [ + 'contextid' => $context->id, + 'contextlevel' => CONTEXT_MODULE, + 'modname' => 'glossary', + ]; + + \core_comment\privacy\provider::get_users_in_context_from_sql( + $userlist, 'com', 'mod_glossary', 'glossary_entry', $sql, $params); + + // Find users with glossary ratings. + $sql = "SELECT ge.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 {glossary} g ON g.id = cm.instance + JOIN {glossary_entries} ge ON ge.glossaryid = g.id + WHERE c.id = :contextid"; + + $params = [ + 'contextid' => $context->id, + 'contextlevel' => CONTEXT_MODULE, + 'modname' => 'glossary', + ]; + + \core_rating\privacy\provider::get_users_in_context_from_sql($userlist, 'rat', 'mod_glossary', 'entry', $sql, $params); + } + /** * Export personal data for the given approved_contextlist. * @@ -324,4 +394,59 @@ public static function delete_data_for_user(approved_contextlist $contextlist) { } } } + + /** + * 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(); + $userids = $userlist->get_userids(); + $instanceid = $DB->get_field('course_modules', 'instance', ['id' => $context->instanceid], MUST_EXIST); + list($userinsql, $userinparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED); + + $glossaryentrieswhere = "glossaryid = :instanceid AND userid {$userinsql}"; + $userinstanceparams = $userinparams + ['instanceid' => $instanceid]; + + $entriesobject = $DB->get_recordset_select('glossary_entries', $glossaryentrieswhere, $userinstanceparams, 'id', 'id'); + $entries = []; + + foreach ($entriesobject as $entry) { + $entries[] = $entry->id; + } + + $entriesobject->close(); + + if (!$entries) { + return; + } + + list($insql, $inparams) = $DB->get_in_or_equal($entries, SQL_PARAMS_NAMED); + + // Delete related entry aliases. + $DB->delete_records_list('glossary_alias', 'entryid', $entries); + + // Delete related entry categories. + $DB->delete_records_list('glossary_entries_categories', 'entryid', $entries); + + // Delete related entry and attachment files. + get_file_storage()->delete_area_files_select($context->id, 'mod_glossary', 'entry', $insql, $inparams); + get_file_storage()->delete_area_files_select($context->id, 'mod_glossary', 'attachment', $insql, $inparams); + + // Delete user tags related to this glossary. + \core_tag\privacy\provider::delete_item_tags_select($context, 'mod_glossary', 'glossary_entries', $insql, $inparams); + + // Delete related ratings. + \core_rating\privacy\provider::delete_ratings_select($context, 'mod_glossary', 'entry', $insql, $inparams); + + // Delete comments. + \core_comment\privacy\provider::delete_comments_for_users($userlist, 'mod_glossary', 'glossary_entry'); + + // Now delete all user related entries. + $deletewhere = "glossaryid = :instanceid AND userid {$userinsql}"; + $DB->delete_records_select('glossary_entries', $deletewhere, $userinstanceparams); + } } diff --git a/mod/glossary/tests/privacy_provider_test.php b/mod/glossary/tests/privacy_provider_test.php index 660c5969425f0..d763fc78ac063 100644 --- a/mod/glossary/tests/privacy_provider_test.php +++ b/mod/glossary/tests/privacy_provider_test.php @@ -30,6 +30,7 @@ global $CFG; require_once($CFG->dirroot . '/comment/lib.php'); +require_once($CFG->dirroot . '/rating/lib.php'); /** * Privacy provider tests class. @@ -131,6 +132,27 @@ public function test_get_contexts_for_userid() { $this->assertEquals($cmcontext->id, $contextforuser->id); } + /** + * Test for provider::get_users_in_context(). + */ + public function test_get_users_in_context() { + $component = 'mod_glossary'; + $cm = get_coursemodule_from_instance('glossary', $this->glossary->id); + $cmcontext = context_module::instance($cm->id); + + $userlist = new \core_privacy\local\request\userlist($cmcontext, $component); + provider::get_users_in_context($userlist); + + $this->assertCount(1, $userlist); + + $expected = [$this->student->id]; + $actual = $userlist->get_userids(); + sort($expected); + sort($actual); + + $this->assertEquals($expected, $actual); + } + /** * Test for provider::export_user_data(). */ @@ -212,6 +234,7 @@ public function test_delete_data_for_user() { global $DB; $generator = $this->getDataGenerator(); + // Create another student who will add an entry to the first glossary. $student2 = $generator->create_user(); $generator->enrol_user($student2->id, $this->course->id, 'student'); @@ -235,6 +258,11 @@ public function test_delete_data_for_user() { core_tag_tag::set_item_tags('mod_glossary', 'glossary_entries', $ge3->id, $context1, ['Pizza', 'Noodles']); + // As a teacher, rate student 2's entry. + $this->setUser($this->teacher); + $rating = $this->get_rating_object($context1, $ge3->id); + $rating->update_rating(2); + // Before deletion, we should have 3 entries, one rating and 2 tag instances. $count = $DB->count_records('glossary_entries', ['glossaryid' => $this->glossary->id]); $this->assertEquals(3, $count); @@ -243,7 +271,10 @@ public function test_delete_data_for_user() { $this->assertEquals(2, $tagcount); $aliascount = $DB->count_records('glossary_alias', ['entryid' => $ge3->id]); $this->assertEquals(1, $aliascount); - // Create another student who will add an entry to the first glossary. + $ratingcount = $DB->count_records('rating', ['component' => 'mod_glossary', 'ratingarea' => 'entry', + 'itemid' => $ge3->id]); + $this->assertEquals(1, $ratingcount); + $contextlist = new \core_privacy\local\request\approved_contextlist($student2, 'glossary', [$context1->id, $context2->id]); provider::delete_data_for_user($contextlist); @@ -274,6 +305,120 @@ public function test_delete_data_for_user() { $commentcount = $DB->count_records('comments', ['component' => 'mod_glossary', 'commentarea' => 'glossary_entry', 'userid' => $this->student->id]); $this->assertEquals(1, $commentcount); + + $ratingcount = $DB->count_records('rating', ['component' => 'mod_glossary', 'ratingarea' => 'entry', + 'itemid' => $ge3->id]); + $this->assertEquals(0, $ratingcount); + } + + /** + * Test for provider::delete_data_for_users(). + */ + public function test_delete_data_for_users() { + global $DB; + $generator = $this->getDataGenerator(); + + $student2 = $generator->create_user(); + $generator->enrol_user($student2->id, $this->course->id, 'student'); + + $cm1 = get_coursemodule_from_instance('glossary', $this->glossary->id); + $glossary2 = $this->plugingenerator->create_instance(['course' => $this->course->id]); + $cm2 = get_coursemodule_from_instance('glossary', $glossary2->id); + + $ge1 = $this->plugingenerator->create_content($this->glossary, ['concept' => 'first user glossary entry', 'approved' => 1]); + $ge2 = $this->plugingenerator->create_content($glossary2, ['concept' => 'first user second glossary entry', + 'approved' => 1], ['two']); + + $context1 = context_module::instance($cm1->id); + $context2 = context_module::instance($cm2->id); + core_tag_tag::set_item_tags('mod_glossary', 'glossary_entries', $ge1->id, $context1, ['Parmi', 'Sushi']); + + $this->setUser($student2); + $ge3 = $this->plugingenerator->create_content($this->glossary, ['concept' => 'second user glossary entry', + 'approved' => 1], ['three']); + + $comment = $this->get_comment_object($context1, $ge3->id); + $comment->add('User 2 comment 1'); + $comment = $this->get_comment_object($context2, $ge2->id); + $comment->add('User 2 comment 2'); + + core_tag_tag::set_item_tags('mod_glossary', 'glossary_entries', $ge3->id, $context1, ['Pizza', 'Noodles']); + core_tag_tag::set_item_tags('mod_glossary', 'glossary_entries', $ge2->id, $context2, ['Potato', 'Kumara']); + + // As a teacher, rate student 2's entry. + $this->setUser($this->teacher); + $rating = $this->get_rating_object($context1, $ge3->id); + $rating->update_rating(2); + + // Check correct glossary 1 record counts before deletion. + $count = $DB->count_records('glossary_entries', ['glossaryid' => $this->glossary->id]); + // Note: There is an additional student entry from setUp(). + $this->assertEquals(3, $count); + + list($context1itemsql, $context1itemparams) = $DB->get_in_or_equal([$ge1->id, $ge3->id], SQL_PARAMS_NAMED); + $geparams = [ + 'component' => 'mod_glossary', + 'itemtype' => 'glossary_entries', + ]; + $geparams += $context1itemparams; + $wheresql = "component = :component AND itemtype = :itemtype AND itemid {$context1itemsql}"; + + $tagcount = $DB->count_records_select('tag_instance', $wheresql, $geparams); + $this->assertEquals(4, $tagcount); + + $aliascount = $DB->count_records_select('glossary_alias', "entryid {$context1itemsql}", $context1itemparams); + $this->assertEquals(1, $aliascount); + + $commentparams = [ + 'component' => 'mod_glossary', + 'commentarea' => 'glossary_entry', + ]; + $commentparams += $context1itemparams; + $commentwhere = "component = :component AND commentarea = :commentarea AND itemid {$context1itemsql}"; + + $commentcount = $DB->count_records_select('comments', $commentwhere, $commentparams); + $this->assertEquals(1, $commentcount); + + $ratingcount = $DB->count_records('rating', ['component' => 'mod_glossary', 'ratingarea' => 'entry', + 'itemid' => $ge3->id]); + $this->assertEquals(1, $ratingcount); + + // Perform deletion within context 1 for both students. + $approveduserlist = new core_privacy\local\request\approved_userlist($context1, 'mod_glossary', + [$this->student->id, $student2->id]); + provider::delete_data_for_users($approveduserlist); + + // After deletion, all context 1 entries, tags and comment should be deleted. + $count = $DB->count_records('glossary_entries', ['glossaryid' => $this->glossary->id]); + $this->assertEquals(0, $count); + + $tagcount = $DB->count_records_select('tag_instance', $wheresql, $geparams); + $this->assertEquals(0, $tagcount); + + $aliascount = $DB->count_records_select('glossary_alias', "entryid {$context1itemsql}", $context1itemparams); + $this->assertEquals(0, $aliascount); + + $commentcount = $DB->count_records_select('comments', $commentwhere, $commentparams); + $this->assertEquals(0, $commentcount); + + // Context 2 entries should remain intact. + $count = $DB->count_records('glossary_entries', ['glossaryid' => $glossary2->id]); + $this->assertEquals(1, $count); + + $tagcount = $DB->count_records('tag_instance', ['component' => 'mod_glossary', 'itemtype' => 'glossary_entries', + 'itemid' => $ge2->id]); + $this->assertEquals(2, $tagcount); + + $aliascount = $DB->count_records('glossary_alias', ['entryid' => $ge2->id]); + $this->assertEquals(1, $aliascount); + + $commentcount = $DB->count_records('comments', ['component' => 'mod_glossary', 'commentarea' => 'glossary_entry', + 'itemid' => $ge2->id]); + $this->assertEquals(1, $commentcount); + + $ratingcount = $DB->count_records('rating', ['component' => 'mod_glossary', 'ratingarea' => 'entry', + 'itemid' => $ge3->id]); + $this->assertEquals(0, $ratingcount); } /**