} else {
$expiryinfo = static::get_expiry_info($purpose, $record->expirydate);
}
+
foreach ($datalist as $path => $data) {
// Merge with already-processed children.
if (strpos($path, $context->path) !== 0) {
$expiryinfo->merge_with_child($data->info);
}
+
$datalist[$context->path] = (object) [
'context' => $context,
'record' => $record,
}
$privacymanager = $this->get_privacy_manager();
- if ($context instanceof \context_user) {
- $this->delete_expired_user_context($expiredctx);
- } else {
- // This context is fully expired - that is that the default retention period has been reached.
- $privacymanager->delete_data_for_all_users_in_context($context);
+ if ($expiredctx->is_fully_expired()) {
+ if ($context instanceof \context_user) {
+ $this->delete_expired_user_context($expiredctx);
+ } else {
+ // This context is fully expired - that is that the default retention period has been reached, and there are
+ // no remaining overrides.
+ $privacymanager->delete_data_for_all_users_in_context($context);
+ }
+
+ // Mark the record as cleaned.
+ $expiredctx->set('status', expired_context::STATUS_CLEANED);
+ $expiredctx->save();
+
+ return $context;
+ }
+
+ // We need to find all users in the context, and delete just those who have expired.
+ $collection = $privacymanager->get_users_in_context($context);
+
+ // Apply the expired and unexpired filters to remove the users in these categories.
+ $userassignments = $this->get_role_users_for_expired_context($expiredctx, $context);
+ $approvedcollection = new \core_privacy\local\request\userlist_collection($context);
+ foreach ($collection as $pendinguserlist) {
+ $userlist = filtered_userlist::create_from_userlist($pendinguserlist);
+ $userlist->apply_expired_context_filters($userassignments->expired, $userassignments->unexpired);
+ if (count($userlist)) {
+ $approvedcollection->add_userlist($userlist);
+ }
+ }
+
+ if (count($approvedcollection)) {
+ // Perform the deletion with the newly approved collection.
+ $privacymanager->delete_data_for_users_in_context($approvedcollection);
}
// Mark the record as cleaned.
* @return expiry_info
*/
protected static function get_expiry_info(purpose $purpose, int $comparisondate = 0) : expiry_info {
- if (empty($comparisondate)) {
- // The date is empty, therefore this context cannot be considered for automatic expiry.
- $defaultexpired = false;
+ $overrides = $purpose->get_purpose_overrides();
+ $expiredroles = $unexpiredroles = [];
+ if (empty($overrides)) {
+ // There are no overrides for this purpose.
+ if (empty($comparisondate)) {
+ // The date is empty, therefore this context cannot be considered for automatic expiry.
+ $defaultexpired = false;
+ } else {
+ $defaultexpired = static::has_expired($purpose->get('retentionperiod'), $comparisondate);
+ }
+
+ return new expiry_info($defaultexpired, [], []);
} else {
- $defaultexpired = static::has_expired($purpose->get('retentionperiod'), $comparisondate);
- }
+ foreach ($overrides as $override) {
+ if (static::has_expired($override->get('retentionperiod'), $comparisondate)) {
+ // This role has expired.
+ $expiredroles[] = $override->get('roleid');
+ } else {
+ // This role has not yet expired.
+ $unexpiredroles[] = $override->get('roleid');
+ }
+ }
+
+ $defaultexpired = false;
+ if (static::has_expired($purpose->get('retentionperiod'), $comparisondate)) {
+ $defaultexpired = true;
+ }
- return new expiry_info($defaultexpired);
+ if ($defaultexpired) {
+ $expiredroles = [];
+ }
+
+ return new expiry_info($defaultexpired, $expiredroles, $unexpiredroles);
+ }
}
/**
* @return expired_context|null
*/
protected function update_from_expiry_info(\stdClass $expiryrecord) {
- if ($expiryrecord->info->is_any_expired()) {
+ if ($isanyexpired = $expiryrecord->info->is_any_expired()) {
// The context is expired in some fashion.
// Create or update as required.
if ($expiryrecord->record->expiredctxid) {
$expiredcontext = expired_context::create_from_expiry_info($expiryrecord->context, $expiryrecord->info);
}
+ if ($expiryrecord->context instanceof \context_user) {
+ $userassignments = $this->get_role_users_for_expired_context($expiredcontext, $expiryrecord->context);
+ if (!empty($userassignments->unexpired)) {
+ $expiredcontext->delete();
+
+ return null;
+ }
+ }
+
return $expiredcontext;
} else {
// The context is not expired.
// Fetch the current nested expiry data.
$expiryrecords = self::get_nested_expiry_info($context->path);
- // Find the current record.
if (empty($expiryrecords[$context->path])) {
$expiredctx->delete();
return null;
return $expiredctx;
}
+ /**
+ * Get the list of actual users for the combination of expired, and unexpired roles.
+ *
+ * @param expired_context $expiredctx
+ * @param \context $context
+ * @return \stdClass
+ */
+ protected function get_role_users_for_expired_context(expired_context $expiredctx, \context $context) : \stdClass {
+ $expiredroles = $expiredctx->get('expiredroles');
+ $expiredroleusers = [];
+ if (!empty($expiredroles)) {
+ // Find the list of expired role users.
+ $expiredroleuserassignments = get_role_users($expiredroles, $context, true, 'ra.id, u.id AS userid', 'ra.id');
+ $expiredroleusers = array_map(function($assignment) {
+ return $assignment->userid;
+ }, $expiredroleuserassignments);
+ }
+ $expiredroleusers = array_unique($expiredroleusers);
+
+ $unexpiredroles = $expiredctx->get('unexpiredroles');
+ $unexpiredroleusers = [];
+ if (!empty($unexpiredroles)) {
+ // Find the list of unexpired role users.
+ $unexpiredroleuserassignments = get_role_users($unexpiredroles, $context, true, 'ra.id, u.id AS userid', 'ra.id');
+ $unexpiredroleusers = array_map(function($assignment) {
+ return $assignment->userid;
+ }, $unexpiredroleuserassignments);
+ }
+ $unexpiredroleusers = array_unique($unexpiredroleusers);
+
+ if (!$expiredctx->get('defaultexpired')) {
+ $tofilter = get_users_roles($context, $expiredroleusers);
+ $tofilter = array_filter($tofilter, function($userroles) use ($expiredroles) {
+ // Each iteration contains the list of role assignment for a specific user.
+ // All roles that the user holds must match those in the list of expired roles.
+ if (count($userroles) === 1) {
+ // Shortcut - only one role held which must be one of the expired roles.
+ // TODO I think this is wrong.
+ return false;
+ }
+
+ foreach ($userroles as $ra) {
+ if (false === array_search($ra->roleid, $expiredroles)) {
+ // This role was not found in the list of assignments.
+ return true;
+ }
+ }
+
+ return false;
+ });
+ $unexpiredroleusers = array_merge($unexpiredroleusers, array_keys($tofilter));
+ }
+
+ return (object) [
+ 'expired' => $expiredroleusers,
+ 'unexpired' => $unexpiredroleusers,
+ ];
+ }
+
/**
* Check whether the course has expired.
*
use tool_dataprivacy\data_registry;
use tool_dataprivacy\expired_context;
use tool_dataprivacy\purpose;
+use tool_dataprivacy\purpose_override;
use tool_dataprivacy\category;
use tool_dataprivacy\contextlevel;
$this->assertEquals(0, $flaggedusers);
}
+ /**
+ * Ensure that a user with a lastaccess in the past and no active enrolments is flagged for deletion.
+ */
+ public function test_flag_user_update_existing() {
+ $this->resetAfterTest();
+
+ $this->setup_basics('PT1H', 'PT1H', 'P5Y');
+
+ $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
+ $usercontext = \context_user::instance($user->id);
+
+ // Create an existing expired_context.
+ $expiredcontext = new expired_context(0, (object) [
+ 'contextid' => $usercontext->id,
+ 'defaultexpired' => 0,
+ 'status' => expired_context::STATUS_EXPIRED,
+ ]);
+ $expiredcontext->save();
+ $this->assertEquals(0, $expiredcontext->get('defaultexpired'));
+
+ // Flag all expired contexts.
+ $manager = new \tool_dataprivacy\expired_contexts_manager();
+ list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
+
+ $this->assertEquals(0, $flaggedcourses);
+ $this->assertEquals(1, $flaggedusers);
+
+ // The user context will now have expired.
+ $updatedcontext = new expired_context($expiredcontext->get('id'));
+ $this->assertEquals(1, $updatedcontext->get('defaultexpired'));
+ }
+
/**
* Ensure that a user with a lastaccess in the past and expired enrolments.
*/
$this->assertEquals(0, $flaggedusers);
}
+ /**
+ * Ensure that a user with a lastaccess in the past and expired enrolments.
+ */
+ public function test_flag_user_past_override_role() {
+ global $DB;
+ $this->resetAfterTest();
+
+ $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
+ $userpurpose = $purposes[1];
+
+ $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
+ $usercontext = \context_user::instance($user->id);
+ $systemcontext = \context_system::instance();
+
+ $role = $DB->get_record('role', ['shortname' => 'manager']);
+
+ $override = new purpose_override(0, (object) [
+ 'purposeid' => $userpurpose->get('id'),
+ 'roleid' => $role->id,
+ 'retentionperiod' => 'P5Y',
+ ]);
+ $override->save();
+ role_assign($role->id, $user->id, $systemcontext->id);
+
+ // Flag all expired contexts.
+ $manager = new \tool_dataprivacy\expired_contexts_manager();
+ list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
+
+ $this->assertEquals(0, $flaggedcourses);
+ $this->assertEquals(0, $flaggedusers);
+
+ $expiredrecord = expired_context::get_record(['contextid' => $usercontext->id]);
+ $this->assertFalse($expiredrecord);
+ }
+
/**
* Ensure that a user with a lastaccess in the past and expired enrolments.
*/
$this->assertEquals(0, $flaggedusers);
}
+ /**
+ * Ensure that a course with an end date in the distant past is flagged, taking into account any purpose override
+ */
+ public function test_flag_course_past_enddate_with_override_unexpired_role() {
+ global $DB;
+ $this->resetAfterTest();
+
+ $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
+ $coursepurpose = $purposes[2];
+
+ $role = $DB->get_record('role', ['shortname' => 'editingteacher']);
+
+ $override = new purpose_override(0, (object) [
+ 'purposeid' => $coursepurpose->get('id'),
+ 'roleid' => $role->id,
+ 'retentionperiod' => 'P5Y',
+ ]);
+ $override->save();
+
+ $course = $this->getDataGenerator()->create_course([
+ 'startdate' => time() - (2 * DAYSECS),
+ 'enddate' => time() - DAYSECS,
+ ]);
+ $coursecontext = \context_course::instance($course->id);
+
+ // Flag all expired contexts.
+ $manager = new \tool_dataprivacy\expired_contexts_manager();
+ list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
+
+ $this->assertEquals(1, $flaggedcourses);
+ $this->assertEquals(0, $flaggedusers);
+
+ $expiredrecord = expired_context::get_record(['contextid' => $coursecontext->id]);
+ $this->assertEmpty($expiredrecord->get('expiredroles'));
+
+ $unexpiredroles = $expiredrecord->get('unexpiredroles');
+ $this->assertCount(1, $unexpiredroles);
+ $this->assertContains($role->id, $unexpiredroles);
+ }
+
+ /**
+ * Ensure that a course with an end date in the distant past is flagged, and any expired role is ignored.
+ */
+ public function test_flag_course_past_enddate_with_override_expired_role() {
+ global $DB;
+ $this->resetAfterTest();
+
+ $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
+ $coursepurpose = $purposes[2];
+
+ $role = $DB->get_record('role', ['shortname' => 'student']);
+
+ // The role has a much shorter retention, but both should match.
+ $override = new purpose_override(0, (object) [
+ 'purposeid' => $coursepurpose->get('id'),
+ 'roleid' => $role->id,
+ 'retentionperiod' => 'PT1M',
+ ]);
+ $override->save();
+
+ $course = $this->getDataGenerator()->create_course([
+ 'startdate' => time() - (2 * DAYSECS),
+ 'enddate' => time() - DAYSECS,
+ ]);
+ $coursecontext = \context_course::instance($course->id);
+
+ // Flag all expired contexts.
+ $manager = new \tool_dataprivacy\expired_contexts_manager();
+ list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
+
+ $this->assertEquals(1, $flaggedcourses);
+ $this->assertEquals(0, $flaggedusers);
+
+ $expiredrecord = expired_context::get_record(['contextid' => $coursecontext->id]);
+ $this->assertEmpty($expiredrecord->get('expiredroles'));
+ $this->assertEmpty($expiredrecord->get('unexpiredroles'));
+ $this->assertTrue((bool) $expiredrecord->get('defaultexpired'));
+ }
+
+ /**
+ * Ensure that where a course has explicitly expired one role, but that role is explicitly not expired in a child
+ * context, does not have the parent context role expired.
+ */
+ public function test_flag_course_override_expiredwith_override_unexpired_on_child() {
+ global $DB;
+ $this->resetAfterTest();
+
+ $purposes = $this->setup_basics('P1Y', 'P1Y', 'P1Y');
+ $coursepurpose = $purposes[2];
+
+ $role = $DB->get_record('role', ['shortname' => 'editingteacher']);
+
+ (new purpose_override(0, (object) [
+ 'purposeid' => $coursepurpose->get('id'),
+ 'roleid' => $role->id,
+ 'retentionperiod' => 'PT1S',
+ ]))->save();
+
+ $modpurpose = new purpose(0, (object) [
+ 'name' => 'Module purpose',
+ 'retentionperiod' => 'PT1S',
+ 'lawfulbases' => 'gdpr_art_6_1_a',
+ ]);
+ $modpurpose->create();
+
+ (new purpose_override(0, (object) [
+ 'purposeid' => $modpurpose->get('id'),
+ 'roleid' => $role->id,
+ 'retentionperiod' => 'P5Y',
+ ]))->save();
+
+ $course = $this->getDataGenerator()->create_course([
+ 'startdate' => time() - (2 * DAYSECS),
+ 'enddate' => time() - DAYSECS,
+ ]);
+ $coursecontext = \context_course::instance($course->id);
+
+ $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
+ $cm = get_coursemodule_from_instance('forum', $forum->id);
+ $forumcontext = \context_module::instance($cm->id);
+
+ api::set_context_instance((object) [
+ 'contextid' => $forumcontext->id,
+ 'purposeid' => $modpurpose->get('id'),
+ 'categoryid' => 0,
+ ]);
+
+ // Flag all expired contexts.
+ $manager = new \tool_dataprivacy\expired_contexts_manager();
+ list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
+
+ $this->assertEquals(1, $flaggedcourses);
+ $this->assertEquals(0, $flaggedusers);
+
+ // The course will not be expired as the default expiry has not passed, and the explicit role override has been
+ // removed due to the child non-expiry.
+ $expiredrecord = expired_context::get_record(['contextid' => $coursecontext->id]);
+ $this->assertFalse($expiredrecord);
+
+ // The forum will have an expiry for all _but_ the overridden role.
+ $expiredrecord = expired_context::get_record(['contextid' => $forumcontext->id]);
+ $this->assertEmpty($expiredrecord->get('expiredroles'));
+
+ // The teacher is not expired.
+ $unexpiredroles = $expiredrecord->get('unexpiredroles');
+ $this->assertCount(1, $unexpiredroles);
+ $this->assertContains($role->id, $unexpiredroles);
+ $this->assertTrue((bool) $expiredrecord->get('defaultexpired'));
+ }
+
+ /**
+ * Ensure that a user context previously flagged as approved is not removed if the user has any unexpired roles.
+ */
+ public function test_process_user_context_with_override_unexpired_role() {
+ global $DB;
+ $this->resetAfterTest();
+
+ $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
+ $userpurpose = $purposes[1];
+
+ $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
+ $usercontext = \context_user::instance($user->id);
+ $systemcontext = \context_system::instance();
+
+ $role = $DB->get_record('role', ['shortname' => 'manager']);
+
+ $override = new purpose_override(0, (object) [
+ 'purposeid' => $userpurpose->get('id'),
+ 'roleid' => $role->id,
+ 'retentionperiod' => 'P5Y',
+ ]);
+ $override->save();
+ role_assign($role->id, $user->id, $systemcontext->id);
+
+ // Create an existing expired_context.
+ $expiredcontext = new expired_context(0, (object) [
+ 'contextid' => $usercontext->id,
+ 'defaultexpired' => 1,
+ 'status' => expired_context::STATUS_APPROVED,
+ ]);
+ $expiredcontext->add_unexpiredroles([$role->id]);
+ $expiredcontext->save();
+
+ $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
+ ->setMethods([
+ 'delete_data_for_user',
+ 'delete_data_for_users_in_context',
+ 'delete_data_for_all_users_in_context',
+ ])
+ ->getMock();
+ $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
+ $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
+ $mockprivacymanager->expects($this->never())->method('delete_data_for_users_in_context');
+
+ $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
+ ->setMethods(['get_privacy_manager'])
+ ->getMock();
+
+ $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
+ $manager->set_progress(new \null_progress_trace());
+ list($processedcourses, $processedusers) = $manager->process_approved_deletions();
+
+ $this->assertEquals(0, $processedcourses);
+ $this->assertEquals(0, $processedusers);
+
+ $this->expectException('dml_missing_record_exception');
+ $updatedcontext = new expired_context($expiredcontext->get('id'));
+ }
+
+ /**
+ * Ensure that a module context previously flagged as approved is removed with appropriate unexpiredroles kept.
+ */
+ public function test_process_course_context_with_override_unexpired_role() {
+ global $DB;
+ $this->resetAfterTest();
+
+ $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
+ $coursepurpose = $purposes[2];
+
+ $role = $DB->get_record('role', ['shortname' => 'editingteacher']);
+
+ $override = new purpose_override(0, (object) [
+ 'purposeid' => $coursepurpose->get('id'),
+ 'roleid' => $role->id,
+ 'retentionperiod' => 'P5Y',
+ ]);
+ $override->save();
+
+ $course = $this->getDataGenerator()->create_course([
+ 'startdate' => time() - (2 * YEARSECS),
+ 'enddate' => time() - YEARSECS,
+ ]);
+ $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
+ $cm = get_coursemodule_from_instance('forum', $forum->id);
+ $forumcontext = \context_module::instance($cm->id);
+ $generator = $this->getDataGenerator()->get_plugin_generator('mod_forum');
+
+ $student = $this->getDataGenerator()->create_user();
+ $this->getDataGenerator()->enrol_user($student->id, $course->id, 'student');
+ $generator->create_discussion((object) [
+ 'course' => $forum->course,
+ 'forum' => $forum->id,
+ 'userid' => $student->id,
+ ]);
+
+ $teacher = $this->getDataGenerator()->create_user();
+ $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'editingteacher');
+ $generator->create_discussion((object) [
+ 'course' => $forum->course,
+ 'forum' => $forum->id,
+ 'userid' => $teacher->id,
+ ]);
+
+ // Create an existing expired_context.
+ $expiredcontext = new expired_context(0, (object) [
+ 'contextid' => $forumcontext->id,
+ 'defaultexpired' => 1,
+ 'status' => expired_context::STATUS_APPROVED,
+ ]);
+ $expiredcontext->add_unexpiredroles([$role->id]);
+ $expiredcontext->save();
+
+ $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
+ ->setMethods([
+ 'delete_data_for_user',
+ 'delete_data_for_users_in_context',
+ 'delete_data_for_all_users_in_context',
+ ])
+ ->getMock();
+ $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
+ $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
+ $mockprivacymanager
+ ->expects($this->once())
+ ->method('delete_data_for_users_in_context')
+ ->with($this->callback(function($userlist) use ($student, $teacher) {
+ $forumlist = $userlist->get_userlist_for_component('mod_forum');
+ $userids = $forumlist->get_userids();
+ $this->assertCount(1, $userids);
+ $this->assertContains($student->id, $userids);
+ $this->assertNotContains($teacher->id, $userids);
+ return true;
+ }));
+
+ $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
+ ->setMethods(['get_privacy_manager'])
+ ->getMock();
+
+ $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
+ $manager->set_progress(new \null_progress_trace());
+ list($processedcourses, $processedusers) = $manager->process_approved_deletions();
+
+ $this->assertEquals(1, $processedcourses);
+ $this->assertEquals(0, $processedusers);
+
+ $updatedcontext = new expired_context($expiredcontext->get('id'));
+ $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
+ }
+
+ /**
+ * Ensure that a module context previously flagged as approved is removed with appropriate expiredroles kept.
+ */
+ public function test_process_course_context_with_override_expired_role() {
+ global $DB;
+ $this->resetAfterTest();
+
+ $purposes = $this->setup_basics('PT1H', 'PT1H', 'P5Y');
+ $coursepurpose = $purposes[2];
+
+ $role = $DB->get_record('role', ['shortname' => 'student']);
+
+ $override = new purpose_override(0, (object) [
+ 'purposeid' => $coursepurpose->get('id'),
+ 'roleid' => $role->id,
+ 'retentionperiod' => 'PT1M',
+ ]);
+ $override->save();
+
+ $course = $this->getDataGenerator()->create_course([
+ 'startdate' => time() - (2 * YEARSECS),
+ 'enddate' => time() - YEARSECS,
+ ]);
+ $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
+ $cm = get_coursemodule_from_instance('forum', $forum->id);
+ $forumcontext = \context_module::instance($cm->id);
+ $generator = $this->getDataGenerator()->get_plugin_generator('mod_forum');
+
+ $student = $this->getDataGenerator()->create_user();
+ $this->getDataGenerator()->enrol_user($student->id, $course->id, 'student');
+ $generator->create_discussion((object) [
+ 'course' => $forum->course,
+ 'forum' => $forum->id,
+ 'userid' => $student->id,
+ ]);
+
+ $teacher = $this->getDataGenerator()->create_user();
+ $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'editingteacher');
+ $generator->create_discussion((object) [
+ 'course' => $forum->course,
+ 'forum' => $forum->id,
+ 'userid' => $teacher->id,
+ ]);
+
+ // Create an existing expired_context.
+ $expiredcontext = new expired_context(0, (object) [
+ 'contextid' => $forumcontext->id,
+ 'defaultexpired' => 0,
+ 'status' => expired_context::STATUS_APPROVED,
+ ]);
+ $expiredcontext->add_expiredroles([$role->id]);
+ $expiredcontext->save();
+
+ $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
+ ->setMethods([
+ 'delete_data_for_user',
+ 'delete_data_for_users_in_context',
+ 'delete_data_for_all_users_in_context',
+ ])
+ ->getMock();
+ $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
+ $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
+ $mockprivacymanager
+ ->expects($this->once())
+ ->method('delete_data_for_users_in_context')
+ ->with($this->callback(function($userlist) use ($student, $teacher) {
+ $forumlist = $userlist->get_userlist_for_component('mod_forum');
+ $userids = $forumlist->get_userids();
+ $this->assertCount(1, $userids);
+ $this->assertContains($student->id, $userids);
+ $this->assertNotContains($teacher->id, $userids);
+ return true;
+ }));
+
+ $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
+ ->setMethods(['get_privacy_manager'])
+ ->getMock();
+
+ $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
+ $manager->set_progress(new \null_progress_trace());
+ list($processedcourses, $processedusers) = $manager->process_approved_deletions();
+
+ $this->assertEquals(1, $processedcourses);
+ $this->assertEquals(0, $processedusers);
+
+ $updatedcontext = new expired_context($expiredcontext->get('id'));
+ $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
+ }
+
+ /**
+ * Ensure that a module context previously flagged as approved is removed with appropriate expiredroles kept.
+ */
+ public function test_process_course_context_with_user_in_both_lists() {
+ global $DB;
+ $this->resetAfterTest();
+
+ $purposes = $this->setup_basics('PT1H', 'PT1H', 'P5Y');
+ $coursepurpose = $purposes[2];
+
+ $role = $DB->get_record('role', ['shortname' => 'student']);
+
+ $override = new purpose_override(0, (object) [
+ 'purposeid' => $coursepurpose->get('id'),
+ 'roleid' => $role->id,
+ 'retentionperiod' => 'PT1M',
+ ]);
+ $override->save();
+
+ $course = $this->getDataGenerator()->create_course([
+ 'startdate' => time() - (2 * YEARSECS),
+ 'enddate' => time() - YEARSECS,
+ ]);
+ $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
+ $cm = get_coursemodule_from_instance('forum', $forum->id);
+ $forumcontext = \context_module::instance($cm->id);
+ $generator = $this->getDataGenerator()->get_plugin_generator('mod_forum');
+
+ $teacher = $this->getDataGenerator()->create_user();
+ $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'editingteacher');
+ $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'student');
+ $generator->create_discussion((object) [
+ 'course' => $forum->course,
+ 'forum' => $forum->id,
+ 'userid' => $teacher->id,
+ ]);
+
+ $student = $this->getDataGenerator()->create_user();
+ $this->getDataGenerator()->enrol_user($student->id, $course->id, 'student');
+ $generator->create_discussion((object) [
+ 'course' => $forum->course,
+ 'forum' => $forum->id,
+ 'userid' => $student->id,
+ ]);
+
+ // Create an existing expired_context.
+ $expiredcontext = new expired_context(0, (object) [
+ 'contextid' => $forumcontext->id,
+ 'defaultexpired' => 0,
+ 'status' => expired_context::STATUS_APPROVED,
+ ]);
+ $expiredcontext->add_expiredroles([$role->id]);
+ $expiredcontext->save();
+
+ $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
+ ->setMethods([
+ 'delete_data_for_user',
+ 'delete_data_for_users_in_context',
+ 'delete_data_for_all_users_in_context',
+ ])
+ ->getMock();
+ $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
+ $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
+ $mockprivacymanager
+ ->expects($this->once())
+ ->method('delete_data_for_users_in_context')
+ ->with($this->callback(function($userlist) use ($student, $teacher) {
+ $forumlist = $userlist->get_userlist_for_component('mod_forum');
+ $userids = $forumlist->get_userids();
+ $this->assertCount(1, $userids);
+ $this->assertContains($student->id, $userids);
+ $this->assertNotContains($teacher->id, $userids);
+ return true;
+ }));
+
+ $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
+ ->setMethods(['get_privacy_manager'])
+ ->getMock();
+
+ $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
+ $manager->set_progress(new \null_progress_trace());
+ list($processedcourses, $processedusers) = $manager->process_approved_deletions();
+
+ $this->assertEquals(1, $processedcourses);
+ $this->assertEquals(0, $processedusers);
+
+ $updatedcontext = new expired_context($expiredcontext->get('id'));
+ $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
+ }
+
+ /**
+ * Ensure that a module context previously flagged as approved is removed with appropriate expiredroles kept.
+ */
+ public function test_process_course_context_with_user_in_both_lists_expired() {
+ global $DB;
+ $this->resetAfterTest();
+
+ $purposes = $this->setup_basics('PT1H', 'PT1H', 'P5Y');
+ $coursepurpose = $purposes[2];
+
+ $studentrole = $DB->get_record('role', ['shortname' => 'student']);
+ $override = new purpose_override(0, (object) [
+ 'purposeid' => $coursepurpose->get('id'),
+ 'roleid' => $studentrole->id,
+ 'retentionperiod' => 'PT1M',
+ ]);
+ $override->save();
+
+ $teacherrole = $DB->get_record('role', ['shortname' => 'editingteacher']);
+ $override = new purpose_override(0, (object) [
+ 'purposeid' => $coursepurpose->get('id'),
+ 'roleid' => $teacherrole->id,
+ 'retentionperiod' => 'PT1M',
+ ]);
+ $override->save();
+
+ $course = $this->getDataGenerator()->create_course([
+ 'startdate' => time() - (2 * YEARSECS),
+ 'enddate' => time() - YEARSECS,
+ ]);
+ $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
+ $cm = get_coursemodule_from_instance('forum', $forum->id);
+ $forumcontext = \context_module::instance($cm->id);
+ $generator = $this->getDataGenerator()->get_plugin_generator('mod_forum');
+
+ $teacher = $this->getDataGenerator()->create_user();
+ $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'editingteacher');
+ $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'student');
+ $generator->create_discussion((object) [
+ 'course' => $forum->course,
+ 'forum' => $forum->id,
+ 'userid' => $teacher->id,
+ ]);
+
+ $student = $this->getDataGenerator()->create_user();
+ $this->getDataGenerator()->enrol_user($student->id, $course->id, 'student');
+ $generator->create_discussion((object) [
+ 'course' => $forum->course,
+ 'forum' => $forum->id,
+ 'userid' => $student->id,
+ ]);
+
+ // Create an existing expired_context.
+ $expiredcontext = new expired_context(0, (object) [
+ 'contextid' => $forumcontext->id,
+ 'defaultexpired' => 0,
+ 'status' => expired_context::STATUS_APPROVED,
+ ]);
+ $expiredcontext->add_expiredroles([$studentrole->id, $teacherrole->id]);
+ $expiredcontext->save();
+
+ $mockprivacymanager = $this->getMockBuilder(\core_privacy\manager::class)
+ ->setMethods([
+ 'delete_data_for_user',
+ 'delete_data_for_users_in_context',
+ 'delete_data_for_all_users_in_context',
+ ])
+ ->getMock();
+ $mockprivacymanager->expects($this->never())->method('delete_data_for_user');
+ $mockprivacymanager->expects($this->never())->method('delete_data_for_all_users_in_context');
+ $mockprivacymanager
+ ->expects($this->once())
+ ->method('delete_data_for_users_in_context')
+ ->with($this->callback(function($userlist) use ($student, $teacher) {
+ $forumlist = $userlist->get_userlist_for_component('mod_forum');
+ $userids = $forumlist->get_userids();
+ $this->assertCount(2, $userids);
+ $this->assertContains($student->id, $userids);
+ $this->assertContains($teacher->id, $userids);
+ return true;
+ }));
+
+ $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
+ ->setMethods(['get_privacy_manager'])
+ ->getMock();
+
+ $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
+ $manager->set_progress(new \null_progress_trace());
+ list($processedcourses, $processedusers) = $manager->process_approved_deletions();
+
+ $this->assertEquals(1, $processedcourses);
+ $this->assertEquals(0, $processedusers);
+
+ $updatedcontext = new expired_context($expiredcontext->get('id'));
+ $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
+ }
+
/**
* Ensure that a site not setup will not process anything.
*/
];
}
+ /**
+ * Test that the is_fully_expired function returns expected results.
+ *
+ * @dataProvider is_fully_expired_provider
+ * @param array $record
+ * @param bool $expected
+ */
+ public function test_is_fully_expired($record, $expected) {
+ $purpose = new expired_context(0, (object) $record);
+
+ $this->assertEquals($expected, $purpose->is_fully_expired());
+ }
+
+ /**
+ * Data provider for the is_fully_expired tests.
+ *
+ * @return array
+ */
+ public function is_fully_expired_provider() : array {
+ return [
+ 'Fully expired' => [
+ [
+ 'status' => expired_context::STATUS_APPROVED,
+ 'defaultexpired' => 1,
+ ],
+ true,
+ ],
+ 'Unexpired roles present' => [
+ [
+ 'status' => expired_context::STATUS_APPROVED,
+ 'defaultexpired' => 1,
+ 'unexpiredroles' => json_encode([1]),
+ ],
+ false,
+ ],
+ 'Only some expired roles present' => [
+ [
+ 'status' => expired_context::STATUS_APPROVED,
+ 'defaultexpired' => 0,
+ 'expiredroles' => json_encode([1]),
+ ],
+ false,
+ ],
+ ];
+ }
+
/**
* Ensure that any orphaned records are removed once the context has been removed.
*/