MDL-63497 mod_data: Add support for removal of context users
authorMichael Hawkins <michaelh@moodle.com>
Thu, 20 Sep 2018 09:17:14 +0000 (17:17 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Wed, 17 Oct 2018 05:12:18 +0000 (13:12 +0800)
This issue is a part of the MDL-62560 Epic.

mod/data/classes/privacy/provider.php
mod/data/tests/privacy_provider_test.php

index b2c36a8..10b6d7b 100644 (file)
@@ -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
      *
index c85c994..31d7c7b 100644 (file)
@@ -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());
+    }
 }