From e9172889711bb23a6dd1537fa464282cfa9acc2d Mon Sep 17 00:00:00 2001 From: Michael Hawkins Date: Mon, 1 Oct 2018 14:46:07 +0800 Subject: [PATCH] MDL-63497 mod_feedback: Add support for removal of context users This issue is a part of the MDL-62560 Epic. --- mod/feedback/classes/privacy/provider.php | 77 ++++++++++++ mod/feedback/tests/privacy_test.php | 137 ++++++++++++++++++++++ 2 files changed, 214 insertions(+) diff --git a/mod/feedback/classes/privacy/provider.php b/mod/feedback/classes/privacy/provider.php index 85b8b009014..0e77a348e34 100644 --- a/mod/feedback/classes/privacy/provider.php +++ b/mod/feedback/classes/privacy/provider.php @@ -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. * diff --git a/mod/feedback/tests/privacy_test.php b/mod/feedback/tests/privacy_test.php index 97fe6c82431..f983481538c 100644 --- a/mod/feedback/tests/privacy_test.php +++ b/mod/feedback/tests/privacy_test.php @@ -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. */