MDL-63496 tool_dataprivacy: Support for per-role retention
This issue is a part of the MDL-62560 Epic.
This commit is contained in:
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user