MDL-63497 mod_feedback: Add support for removal of context users
authorMichael Hawkins <michaelh@moodle.com>
Mon, 1 Oct 2018 06:46:07 +0000 (14:46 +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.

mod/feedback/classes/privacy/provider.php
mod/feedback/tests/privacy_test.php

index 85b8b00..0e77a34 100644 (file)
@@ -31,9 +31,11 @@ use context_helper;
 use stdClass;
 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;
 
 require_once($CFG->dirroot . '/mod/feedback/lib.php');
@@ -48,6 +50,7 @@ require_once($CFG->dirroot . '/mod/feedback/lib.php');
  */
 class provider implements
     \core_privacy\local\metadata\provider,
+    \core_privacy\local\request\core_userlist_provider,
     \core_privacy\local\request\plugin\provider {
 
     /**
@@ -102,6 +105,38 @@ 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 feedback entries.
+        $sql = "
+            SELECT fc.userid
+              FROM {%s} fc
+              JOIN {modules} m
+                ON m.name = :feedback
+              JOIN {course_modules} cm
+                ON cm.instance = fc.feedback
+               AND cm.module = m.id
+              JOIN {context} ctx
+                ON ctx.instanceid = cm.id
+               AND ctx.contextlevel = :modlevel
+             WHERE ctx.id = :contextid";
+        $params = ['feedback' => 'feedback', 'modlevel' => CONTEXT_MODULE, 'contextid' => $context->id];
+
+        $userlist->add_from_sql('userid', sprintf($sql, 'feedback_completed'), $params);
+        $userlist->add_from_sql('userid', sprintf($sql, 'feedback_completedtmp'), $params);
+    }
+
     /**
      * Export all user data for the specified user, in the specified contexts.
      *
@@ -272,6 +307,48 @@ 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();
+
+        // Prepare SQL to gather all completed IDs.
+        list($insql, $inparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
+        $completedsql = "
+            SELECT fc.id
+              FROM {%s} fc
+              JOIN {modules} m
+                ON m.name = :feedback
+              JOIN {course_modules} cm
+                ON cm.instance = fc.feedback
+               AND cm.module = m.id
+             WHERE cm.id = :instanceid
+               AND fc.userid $insql";
+        $completedparams = array_merge($inparams, ['instanceid' => $context->instanceid, 'feedback' => 'feedback']);
+
+        // Delete all submissions in progress.
+        $completedtmpids = $DB->get_fieldset_sql(sprintf($completedsql, 'feedback_completedtmp'), $completedparams);
+        if (!empty($completedtmpids)) {
+            list($insql, $inparams) = $DB->get_in_or_equal($completedtmpids, SQL_PARAMS_NAMED);
+            $DB->delete_records_select('feedback_valuetmp', "completed $insql", $inparams);
+            $DB->delete_records_select('feedback_completedtmp', "id $insql", $inparams);
+        }
+
+        // Delete all final submissions.
+        $completedids = $DB->get_fieldset_sql(sprintf($completedsql, 'feedback_completed'), $completedparams);
+        if (!empty($completedids)) {
+            list($insql, $inparams) = $DB->get_in_or_equal($completedids, SQL_PARAMS_NAMED);
+            $DB->delete_records_select('feedback_value', "completed $insql", $inparams);
+            $DB->delete_records_select('feedback_completed', "id $insql", $inparams);
+        }
+    }
+
     /**
      * Extract an item record from a database record.
      *
index 97fe6c8..f983481 100644 (file)
@@ -115,6 +115,83 @@ class mod_feedback_privacy_testcase extends provider_testcase {
         $this->assertTrue(in_array(context_module::instance($cm2c->cmid)->id, $contextids));
     }
 
+    /**
+     * Test getting the users in a context.
+     */
+    public function test_get_users_in_context() {
+        global $DB;
+        $dg = $this->getDataGenerator();
+        $fg = $dg->get_plugin_generator('mod_feedback');
+        $component = 'mod_feedback';
+
+        $c1 = $dg->create_course();
+        $c2 = $dg->create_course();
+        $cm0 = $dg->create_module('feedback', ['course' => SITEID]);
+        $cm1a = $dg->create_module('feedback', ['course' => $c1, 'anonymous' => FEEDBACK_ANONYMOUS_NO]);
+        $cm1b = $dg->create_module('feedback', ['course' => $c1]);
+        $cm2 = $dg->create_module('feedback', ['course' => $c2]);
+
+        $u1 = $dg->create_user();
+        $u2 = $dg->create_user();
+
+        foreach ([$cm0, $cm1a, $cm1b, $cm2] as $feedback) {
+            $i1 = $fg->create_item_numeric($feedback);
+            $i2 = $fg->create_item_multichoice($feedback);
+            $answers = ['numeric_' . $i1->id => '1', 'multichoice_' . $i2->id => [1]];
+
+            if ($feedback == $cm1b) {
+                $this->create_submission_with_answers($feedback, $u2, $answers);
+            } else {
+                $this->create_submission_with_answers($feedback, $u1, $answers);
+            }
+        }
+
+        // Unsaved submission for u2 in cm1a.
+        $feedback = $cm1a;
+        $i1 = $fg->create_item_numeric($feedback);
+        $i2 = $fg->create_item_multichoice($feedback);
+        $answers = ['numeric_' . $i1->id => '1', 'multichoice_' . $i2->id => [1]];
+        $this->create_tmp_submission_with_answers($feedback, $u2, $answers);
+
+        // Only u1 in cm0.
+        $context = context_module::instance($cm0->cmid);
+        $userlist = new \core_privacy\local\request\userlist($context, $component);
+        provider::get_users_in_context($userlist);
+
+        $this->assertCount(1, $userlist);
+        $this->assertEquals([$u1->id], $userlist->get_userids());
+
+        $context = context_module::instance($cm1a->cmid);
+        $userlist = new \core_privacy\local\request\userlist($context, $component);
+        provider::get_users_in_context($userlist);
+
+        // Two submissions in cm1a: saved for u1, unsaved for u2.
+        $this->assertCount(2, $userlist);
+
+        $expected = [$u1->id, $u2->id];
+        $actual = $userlist->get_userids();
+        sort($expected);
+        sort($actual);
+
+        $this->assertEquals($expected, $actual);
+
+        // Only u2 in cm1b.
+        $context = context_module::instance($cm1b->cmid);
+        $userlist = new \core_privacy\local\request\userlist($context, $component);
+        provider::get_users_in_context($userlist);
+
+        $this->assertCount(1, $userlist);
+        $this->assertEquals([$u2->id], $userlist->get_userids());
+
+        // Only u1 in cm2.
+        $context = context_module::instance($cm2->cmid);
+        $userlist = new \core_privacy\local\request\userlist($context, $component);
+        provider::get_users_in_context($userlist);
+
+        $this->assertCount(1, $userlist);
+        $this->assertEquals([$u1->id], $userlist->get_userids());
+    }
+
     /**
      * Test deleting user data.
      */
@@ -169,6 +246,66 @@ class mod_feedback_privacy_testcase extends provider_testcase {
 
     }
 
+    /**
+     * Test deleting data within a context for an approved userlist.
+     */
+    public function test_delete_data_for_users() {
+        global $DB;
+        $dg = $this->getDataGenerator();
+        $fg = $dg->get_plugin_generator('mod_feedback');
+
+        $c1 = $dg->create_course();
+        $c2 = $dg->create_course();
+        $cm0 = $dg->create_module('feedback', ['course' => SITEID]);
+        $cm1 = $dg->create_module('feedback', ['course' => $c1, 'anonymous' => FEEDBACK_ANONYMOUS_NO]);
+        $cm2 = $dg->create_module('feedback', ['course' => $c2]);
+        $context0 = context_module::instance($cm0->cmid);
+        $context1 = context_module::instance($cm1->cmid);
+
+        $u1 = $dg->create_user();
+        $u2 = $dg->create_user();
+
+        // Create a bunch of data.
+        foreach ([$cm0, $cm1, $cm2] as $feedback) {
+            $i1 = $fg->create_item_numeric($feedback);
+            $i2 = $fg->create_item_multichoice($feedback);
+            $answers = ['numeric_' . $i1->id => '1', 'multichoice_' . $i2->id => [1]];
+
+            $this->create_submission_with_answers($feedback, $u1, $answers);
+            $this->create_tmp_submission_with_answers($feedback, $u1, $answers);
+
+            $this->create_submission_with_answers($feedback, $u2, $answers);
+            $this->create_tmp_submission_with_answers($feedback, $u2, $answers);
+        }
+
+        // Delete u1 from cm0, ensure u2 data is retained.
+        $approveduserlist = new core_privacy\local\request\approved_userlist($context0, 'mod_feedback', [$u1->id]);
+        provider::delete_data_for_users($approveduserlist);
+
+        $this->assert_no_feedback_data_for_user($cm0, $u1);
+        $this->assert_feedback_data_for_user($cm0, $u2);
+        $this->assert_feedback_tmp_data_for_user($cm0, $u2);
+
+        // Ensure cm1 unaffected by cm1 deletes.
+        $this->assert_feedback_data_for_user($cm1, $u1);
+        $this->assert_feedback_tmp_data_for_user($cm1, $u1);
+        $this->assert_feedback_data_for_user($cm1, $u2);
+        $this->assert_feedback_tmp_data_for_user($cm1, $u2);
+
+        // Delete u1 and u2 from cm1, ensure no data is retained.
+        $approveduserlist = new core_privacy\local\request\approved_userlist($context1, 'mod_feedback', [$u1->id, $u2->id]);
+        provider::delete_data_for_users($approveduserlist);
+
+        $this->assert_no_feedback_data_for_user($cm1, $u1);
+        $this->assert_no_feedback_data_for_user($cm1, $u2);
+
+        // Ensure cm2 is unaffected by any of the deletes.
+        $this->assert_feedback_data_for_user($cm2, $u1);
+        $this->assert_feedback_tmp_data_for_user($cm2, $u1);
+        $this->assert_feedback_data_for_user($cm2, $u2);
+        $this->assert_feedback_tmp_data_for_user($cm2, $u2);
+    }
+
     /**
      * Test deleting a whole context.
      */