MDL-63496 tool_dataprivacy: Support for per-role retention

This issue is a part of the MDL-62560 Epic.
This commit is contained in:
Andrew Nicols
2018-10-17 13:13:49 +08:00
parent 744ee9322c
commit 7f7e78269f
10 changed files with 1346 additions and 31 deletions
@@ -60,12 +60,27 @@ class expired_context extends \core\persistent {
* @return array
*/
protected static function define_properties() {
return array(
'contextid' => array(
return [
'contextid' => [
'type' => PARAM_INT,
'description' => 'The context id.',
),
'status' => array(
],
'defaultexpired' => [
'type' => PARAM_INT,
'description' => 'Whether to default retention period for the purpose has been reached',
'default' => 1,
],
'expiredroles' => [
'type' => PARAM_TEXT,
'description' => 'This list of roles to include during deletion',
'default' => '',
],
'unexpiredroles' => [
'type' => PARAM_TEXT,
'description' => 'This list of roles to exclude during deletion',
'default' => '',
],
'status' => [
'choices' => [
self::STATUS_EXPIRED,
self::STATUS_APPROVED,
@@ -73,8 +88,8 @@ class expired_context extends \core\persistent {
],
'type' => PARAM_INT,
'description' => 'The deletion status of the context.',
),
);
],
];
}
/**
@@ -160,21 +175,130 @@ class expired_context extends \core\persistent {
return $DB->count_records_sql($sql, $params);
}
/**
* Set the list of role IDs for either expiredroles, or unexpiredroles.
*
* @param string $field
* @param int[] $roleids
* @return expired_context
*/
protected function set_roleids_for(string $field, array $roleids) : expired_context {
$roledata = json_encode($roleids);
$this->raw_set($field, $roledata);
return $this;
}
/**
* Get the list of role IDs for either expiredroles, or unexpiredroles.
*
* @param string $field
* @return int[]
*/
protected function get_roleids_for(string $field) {
$value = $this->raw_get($field);
if (empty($value)) {
return [];
}
return json_decode($value);
}
/**
* Set the list of unexpired role IDs.
*
* @param int[] $roleids
* @return expired_context
*/
protected function set_unexpiredroles(array $roleids) : expired_context {
$this->set_roleids_for('unexpiredroles', $roleids);
return $this;
}
/**
* Add a set of role IDs to the list of expired role IDs.
*
* @param int[] $roleids
* @return expired_context
*/
public function add_expiredroles(array $roleids) : expired_context {
$existing = $this->get('expiredroles');
$newvalue = array_merge($existing, $roleids);
$this->set('expiredroles', $newvalue);
return $this;
}
/**
* Add a set of role IDs to the list of unexpired role IDs.
*
* @param int[] $roleids
* @return unexpired_context
*/
public function add_unexpiredroles(array $roleids) : expired_context {
$existing = $this->get('unexpiredroles');
$newvalue = array_merge($existing, $roleids);
$this->set('unexpiredroles', $newvalue);
return $this;
}
/**
* Set the list of expired role IDs.
*
* @param int[] $roleids
* @return expired_context
*/
protected function set_expiredroles(array $roleids) : expired_context {
$this->set_roleids_for('expiredroles', $roleids);
return $this;
}
/**
* Get the list of expired role IDs.
*
* @return int[]
*/
protected function get_expiredroles() {
return $this->get_roleids_for('expiredroles');
}
/**
* Get the list of unexpired role IDs.
*
* @return int[]
*/
protected function get_unexpiredroles() {
return $this->get_roleids_for('unexpiredroles');
}
/**
* Create a new expired_context based on the context, and expiry_info object.
*
* @param \context $context
* @param expiry_info $info
* @param boolean $save
* @return expired_context
*/
public static function create_from_expiry_info(\context $context, expiry_info $info) : expired_context {
public static function create_from_expiry_info(\context $context, expiry_info $info, bool $save = true) : expired_context {
$record = (object) [
'contextid' => $context->id,
'status' => self::STATUS_EXPIRED,
'defaultexpired' => (int) $info->is_default_expired(),
];
$expiredcontext = new static(0, $record);
$expiredcontext->save();
$expiredcontext->set('expiredroles', $info->get_expired_roles());
$expiredcontext->set('unexpiredroles', $info->get_unexpired_roles());
if ($save) {
$expiredcontext->save();
}
return $expiredcontext;
}
@@ -186,7 +310,42 @@ class expired_context extends \core\persistent {
* @return $this
*/
public function update_from_expiry_info(expiry_info $info) : expired_context {
$save = false;
// Compare the expiredroles.
$thisexpired = $this->get('expiredroles');
$infoexpired = $info->get_expired_roles();
sort($thisexpired);
sort($infoexpired);
if ($infoexpired != $thisexpired) {
$this->set('expiredroles', $infoexpired);
$save = true;
}
// Compare the unexpiredroles.
$thisunexpired = $this->get('unexpiredroles');
$infounexpired = $info->get_unexpired_roles();
sort($thisunexpired);
sort($infounexpired);
if ($infounexpired != $thisunexpired) {
$this->set('unexpiredroles', $infounexpired);
$save = true;
}
if (empty($this->get('defaultexpired')) == $info->is_default_expired()) {
$this->set('defaultexpired', (int) $info->is_default_expired());
$save = true;
}
if ($save) {
$this->set('status', self::STATUS_EXPIRED);
$this->save();
}
return $this;
}
/**
@@ -206,4 +365,14 @@ class expired_context extends \core\persistent {
public function is_complete() : bool {
return ($this->get('status') == self::STATUS_CLEANED);
}
/**
* Whether this context has 'fully' expired.
* That is to say that the default retention period has been reached, and that there are no unexpired roles.
*
* @return bool
*/
public function is_fully_expired() : bool {
return $this->get('defaultexpired') && empty($this->get('unexpiredroles'));
}
}
@@ -271,6 +271,7 @@ class expired_contexts_manager {
} 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) {
@@ -279,6 +280,7 @@ class expired_contexts_manager {
$expiryinfo->merge_with_child($data->info);
}
$datalist[$context->path] = (object) [
'context' => $context,
'record' => $record,
@@ -425,11 +427,39 @@ class expired_contexts_manager {
}
$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.
@@ -545,14 +575,40 @@ class expired_contexts_manager {
* @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;
} else {
$defaultexpired = static::has_expired($purpose->get('retentionperiod'), $comparisondate);
}
$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);
return new expiry_info($defaultexpired, [], []);
} else {
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;
}
if ($defaultexpired) {
$expiredroles = [];
}
return new expiry_info($defaultexpired, $expiredroles, $unexpiredroles);
}
}
/**
@@ -565,7 +621,7 @@ class expired_contexts_manager {
* @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) {
@@ -579,6 +635,15 @@ class expired_contexts_manager {
$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.
@@ -608,7 +673,6 @@ class expired_contexts_manager {
// 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;
@@ -650,6 +714,65 @@ class expired_contexts_manager {
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.
*
+82 -6
View File
@@ -36,15 +36,28 @@ defined('MOODLE_INTERNAL') || die();
class expiry_info {
/** @var bool Whether this context is fully expired */
protected $isexpired = false;
protected $fullyexpired = false;
/** @var bool Whether the default expiry value of this purpose has been reached */
protected $defaultexpiryreached = false;
/** @var int[] List of expires roles */
protected $expired = [];
/** @var int[] List of unexpires roles */
protected $unexpired = [];
/**
* Constructor for the expiry_info class.
*
* @param bool $isexpired Whether the retention period for this context has expired yet.
* @param bool $default Whether the default expiry period for this context has been reached.
* @param int[] $expired A list of roles in this context which have explicitly expired.
* @param int[] $unexpired A list of roles in this context which have not yet expired.
*/
public function __construct(bool $isexpired) {
$this->isexpired = $isexpired;
public function __construct(bool $default, array $expired, array $unexpired) {
$this->defaultexpiryreached = $default;
$this->expired = $expired;
$this->unexpired = $unexpired;
}
/**
@@ -54,7 +67,7 @@ class expiry_info {
* @return bool
*/
public function is_fully_expired() : bool {
return $this->isexpired;
return $this->defaultexpiryreached && empty($this->unexpired);
}
/**
@@ -67,9 +80,59 @@ class expiry_info {
return true;
}
if (!empty($this->get_expired_roles())) {
return true;
}
if ($this->is_default_expired()) {
return true;
}
return false;
}
/**
* Get the list of explicitly expired role IDs.
* Note: This does not list roles which have been expired via the default retention policy being reached.
*
* @return int[]
*/
public function get_expired_roles() : array {
if ($this->is_default_expired()) {
return [];
}
return $this->expired;
}
/**
* Check whether the specified role is explicitly expired.
* Note: This does not list roles which have been expired via the default retention policy being reached.
*
* @param int $roleid
* @return bool
*/
public function is_role_expired(int $roleid) : bool {
return false !== array_search($roleid, $this->expired);
}
/**
* Whether the default retention policy has been reached.
*
* @return bool
*/
public function is_default_expired() : bool {
return $this->defaultexpiryreached;
}
/**
* Get the list of unexpired role IDs.
*
* @return int[]
*/
public function get_unexpired_roles() : array {
return $this->unexpired;
}
/**
* Merge this expiry_info object with another belonging to a child context in order to set the 'safest' heritage.
*
@@ -86,7 +149,20 @@ class expiry_info {
}
// If the child is not fully expired, then none of the parents can be either.
$this->isexpired = false;
$this->fullyexpired = false;
// Remove any role in this node which is not expired in the child.
foreach ($this->expired as $key => $roleid) {
if (!$child->is_role_expired($roleid)) {
unset($this->expired[$key]);
}
}
array_merge($this->unexpired, $child->get_unexpired_roles());
if (!$child->is_default_expired()) {
$this->defaultexpiryreached = false;
}
return $this;
}
@@ -0,0 +1,63 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* An implementation of a userlist which has been filtered and approved.
*
* @package tool_dataprivacy
* @copyright 2018 Andrew Nicols <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_dataprivacy;
defined('MOODLE_INTERNAL') || die();
/**
* An implementation of a userlist which can be filtered by role.
*
* @copyright 2018 Andrew Nicols <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class filtered_userlist extends \core_privacy\local\request\approved_userlist {
/**
* Apply filters to only remove users in the expireduserids list, and to remove any who are in the unexpired list.
* The unexpired list wins where a user is in both lists.
*
* @param int[] $expireduserids The list of userids for users who should be expired.
* @param int[] $unexpireduserids The list of userids for those users who should not be expired.
* @return $this
*/
public function apply_expired_context_filters(array $expireduserids, array $unexpireduserids) : filtered_userlist {
// The current userlist content.
$userids = $this->get_userids();
if (!empty($expireduserids)) {
// Now remove any not on the list of expired users.
$userids = array_intersect($userids, $expireduserids);
}
if (!empty($unexpireduserids)) {
// Remove any on the list of unexpiredusers users.
$userids = array_diff($userids, $unexpireduserids);
}
$this->set_userids($userids);
return $this;
}
}
+9 -1
View File
@@ -162,7 +162,6 @@ class purpose extends \core\persistent {
* @return null
*/
public function is_used() {
if (\tool_dataprivacy\contextlevel::is_purpose_used($this->get('id')) ||
\tool_dataprivacy\context_instance::is_purpose_used($this->get('id'))) {
return true;
@@ -180,4 +179,13 @@ class purpose extends \core\persistent {
return false;
}
/**
* Get a list of the role purpose overrides for this purpose.
*
* @return array
*/
public function get_purpose_overrides() : array {
return purpose_override::get_overrides_for_purpose($this);
}
}
@@ -0,0 +1,99 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Class for loading/storing data purpose overrides from the DB.
*
* @package tool_dataprivacy
* @copyright 2018 Andrew Nicols <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_dataprivacy;
use stdClass;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php');
/**
* Class for loading/storing data purpose overrides from the DB.
*
* @copyright 2018 Andrew Nicols <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class purpose_override extends \core\persistent {
/**
* Database table.
*/
const TABLE = 'tool_dataprivacy_purposerole';
/**
* Return the definition of the properties of this model.
*
* @return array
*/
protected static function define_properties() {
return array(
'purposeid' => array(
'type' => PARAM_INT,
'description' => 'The purpose that that this override relates to',
),
'roleid' => array(
'type' => PARAM_INT,
'description' => 'The role that that this override relates to',
),
'lawfulbases' => array(
'type' => PARAM_TEXT,
'description' => 'Comma-separated IDs matching records in tool_dataprivacy_lawfulbasis.',
'null' => NULL_ALLOWED,
'default' => null,
),
'sensitivedatareasons' => array(
'type' => PARAM_TEXT,
'description' => 'Comma-separated IDs matching records in tool_dataprivacy_sensitive',
'null' => NULL_ALLOWED,
'default' => null,
),
'retentionperiod' => array(
'type' => PARAM_ALPHANUM,
'description' => 'Retention period. ISO_8601 durations format (as in DateInterval format).',
'default' => '',
),
'protected' => array(
'type' => PARAM_INT,
'description' => 'Data retention with higher precedent over user\'s request to be forgotten.',
'default' => '0',
),
);
}
/**
* Get all role overrides for the purpose.
*
* @param purpose $purpose
* @return array
*/
public static function get_overrides_for_purpose(purpose $purpose) : array {
$overrides = [];
foreach (self::get_records(['purposeid' => $purpose->get('id')]) as $override) {
$overrides[$override->get('roleid')] = $override;
}
return $overrides;
}
}
+26 -1
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="admin/tool/dataprivacy/db" VERSION="20180821" COMMENT="XMLDB file for Moodle tool/dataprivacy"
<XMLDB PATH="admin/tool/dataprivacy/db" VERSION="20180904" COMMENT="XMLDB file for Moodle tool/dataprivacy"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../../lib/xmldb/xmldb.xsd"
>
@@ -98,6 +98,9 @@
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="contextid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="unexpiredroles" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Roles which have explicitly not expired yet."/>
<FIELD NAME="expiredroles" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Explicitly expires roles"/>
<FIELD NAME="defaultexpired" TYPE="int" LENGTH="1" NOTNULL="true" SEQUENCE="false" COMMENT="The default retention period has passed."/>
<FIELD NAME="status" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="usermodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
@@ -146,5 +149,27 @@
<KEY NAME="request_contextlist" TYPE="unique" FIELDS="requestid, contextlistid" COMMENT="Uniqueness constraint on request and contextlist"/>
</KEYS>
</TABLE>
<TABLE NAME="tool_dataprivacy_purposerole" COMMENT="Data purpose overrides for a specific role">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="purposeid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="roleid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="lawfulbases" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="sensitivedatareasons" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="retentionperiod" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="protected" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="usermodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="purposepurposeid" TYPE="foreign" FIELDS="purposeid" REFTABLE="tool_dataprivacy_purpose" REFFIELDS="id"/>
<KEY NAME="puproseroleid" TYPE="foreign" FIELDS="roleid" REFTABLE="role" REFFIELDS="id"/>
</KEYS>
<INDEXES>
<INDEX NAME="purposerole" UNIQUE="true" FIELDS="purposeid, roleid"/>
</INDEXES>
</TABLE>
</TABLES>
</XMLDB>
+64
View File
@@ -184,5 +184,69 @@ function xmldb_tool_dataprivacy_upgrade($oldversion) {
upgrade_plugin_savepoint(true, 2018051407, 'tool', 'dataprivacy');
}
if ($oldversion < 2018051411) {
// Define table tool_dataprivacy_purposerole to be created.
$table = new xmldb_table('tool_dataprivacy_purposerole');
// Adding fields to table tool_dataprivacy_purposerole.
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
$table->add_field('purposeid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
$table->add_field('roleid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
$table->add_field('lawfulbases', XMLDB_TYPE_TEXT, null, null, null, null, null);
$table->add_field('sensitivedatareasons', XMLDB_TYPE_TEXT, null, null, null, null, null);
$table->add_field('retentionperiod', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null);
$table->add_field('protected', XMLDB_TYPE_INTEGER, '1', null, null, null, null);
$table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
$table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
$table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
// Adding keys to table tool_dataprivacy_purposerole.
$table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
$table->add_key('purposepurposeid', XMLDB_KEY_FOREIGN, ['purposeid'], 'tool_dataprivacy_purpose', ['id']);
$table->add_key('puproseroleid', XMLDB_KEY_FOREIGN, ['roleid'], 'role', ['id']);
// Adding indexes to table tool_dataprivacy_purposerole.
$table->add_index('purposerole', XMLDB_INDEX_UNIQUE, ['purposeid', 'roleid']);
// Conditionally launch create table for tool_dataprivacy_purposerole.
if (!$dbman->table_exists($table)) {
$dbman->create_table($table);
}
// Update the ctxexpired table.
$table = new xmldb_table('tool_dataprivacy_ctxexpired');
// Add the unexpiredroles field.
$field = new xmldb_field('unexpiredroles', XMLDB_TYPE_TEXT, null, null, null, null, null, 'contextid');
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
$DB->set_field('tool_dataprivacy_ctxexpired', 'unexpiredroles', '');
// Add the expiredroles field.
$field = new xmldb_field('expiredroles', XMLDB_TYPE_TEXT, null, null, null, null, null, 'unexpiredroles');
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
$DB->set_field('tool_dataprivacy_ctxexpired', 'expiredroles', '');
// Add the defaultexpired field.
$field = new xmldb_field('defaultexpired', XMLDB_TYPE_INTEGER, '1', null, null, null, '1', 'expiredroles');
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Change the default for the expired field to be empty.
$field = new xmldb_field('defaultexpired', XMLDB_TYPE_INTEGER, '1', null, null, null, null, 'expiredroles');
$dbman->change_field_default($table, $field);
// Prevent hte field from being nullable.
$field = new xmldb_field('defaultexpired', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, null, 'expiredroles');
$dbman->change_field_notnull($table, $field);
// Dataprivacy savepoint reached.
upgrade_plugin_savepoint(true, 2018051411, 'tool', 'dataprivacy');
}
return true;
}
@@ -26,6 +26,7 @@ use tool_dataprivacy\api;
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;
@@ -217,6 +218,38 @@ class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
$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.
*/
@@ -245,6 +278,41 @@ class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
$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.
*/
@@ -604,6 +672,580 @@ class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
$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.
*/
@@ -1354,6 +1996,52 @@ class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
];
}
/**
* 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.
*/
+1 -1
View File
@@ -24,6 +24,6 @@
defined('MOODLE_INTERNAL') || die;
$plugin->version = 2018051410;
$plugin->version = 2018051411;
$plugin->requires = 2018050800; // Moodle 3.5dev (Build 2018031600) and upwards.
$plugin->component = 'tool_dataprivacy';