MDL-63497 mod_glossary: Add support for removal of context users
authorMichael Hawkins <michaelh@moodle.com>
Fri, 21 Sep 2018 07:05:10 +0000 (15:05 +0800)
committerDavid Monllao <davidm@moodle.com>
Mon, 22 Oct 2018 10:48:33 +0000 (12:48 +0200)
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
mod/glossary/tests/privacy_provider_test.php

index 691fe68..4c630f6 100644 (file)
 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 @@ defined('MOODLE_INTERNAL') || die();
 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 @@ 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 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 @@ class provider implements
             }
         }
     }
+
+    /**
+     * 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);
+    }
 }
index 660c596..d763fc7 100644 (file)
@@ -30,6 +30,7 @@ defined('MOODLE_INTERNAL') || die();
 
 global $CFG;
 require_once($CFG->dirroot . '/comment/lib.php');
+require_once($CFG->dirroot . '/rating/lib.php');
 
 /**
  * Privacy provider tests class.
@@ -131,6 +132,27 @@ class mod_glossary_privacy_provider_testcase extends \core_privacy\tests\provide
         $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 @@ class mod_glossary_privacy_provider_testcase extends \core_privacy\tests\provide
         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 @@ class mod_glossary_privacy_provider_testcase extends \core_privacy\tests\provide
 
         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 @@ class mod_glossary_privacy_provider_testcase extends \core_privacy\tests\provide
         $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 @@ class mod_glossary_privacy_provider_testcase extends \core_privacy\tests\provide
         $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);
     }
 
     /**