'non-interactive' => false,
'allow-unstable' => false,
'help' => false,
- 'lang' => $lang
+ 'lang' => $lang,
+ 'verbose-settings' => false
),
array(
'h' => 'help'
site language if not set. Defaults to 'en' if the lang
parameter is invalid or if the language pack is not
installed.
+--verbose-settings Show new settings values. By default only the name of
+ new core or plugin settings are displayed. This option
+ outputs the new values as well as the setting name.
-h, --help Print out this help
Example:
// log in as admin - we need doanything permission when applying defaults
\core\session\manager::set_user(get_admin());
-// apply all default settings, just in case do it twice to fill all defaults
-admin_apply_default_settings(NULL, false);
-admin_apply_default_settings(NULL, false);
+// Apply default settings and output those that have changed.
+cli_heading(get_string('cliupgradedefaultheading', 'admin'));
+$settingsoutput = admin_apply_default_settings(null, false);
+
+foreach ($settingsoutput as $setting => $value) {
+
+ if ($options['verbose-settings']) {
+ $stringvlaues = array(
+ 'name' => $setting,
+ 'defaultsetting' => var_export($value, true) // Expand objects.
+ );
+ echo get_string('cliupgradedefaultverbose', 'admin', $stringvlaues) . PHP_EOL;
+
+ } else {
+ echo get_string('cliupgradedefault', 'admin', $setting) . PHP_EOL;
+
+ }
+}
// This needs to happen at the end to ensure it occurs after all caches
// have been purged for the last time.
*/
public static function add_request_contexts_with_status(contextlist_collection $clcollection, int $requestid, int $status) {
$request = new data_request($requestid);
+ $user = \core_user::get_user($request->get('userid'));
foreach ($clcollection as $contextlist) {
// Convert the \core_privacy\local\request\contextlist into a contextlist persistent and store it.
$clp = \tool_dataprivacy\contextlist::from_contextlist($contextlist);
foreach ($contextlist->get_contextids() as $contextid) {
if ($request->get('type') == static::DATAREQUEST_TYPE_DELETE) {
$context = \context::instance_by_id($contextid);
- if (($purpose = static::get_effective_context_purpose($context)) && !empty($purpose->get('protected'))) {
+ $purpose = static::get_effective_context_purpose($context);
+
+ // Data can only be deleted from it if the context is either expired, or unprotected.
+ if (!expired_contexts_manager::is_context_expired_or_unprotected_for_user($context, $user)) {
continue;
}
}
+
$context = new contextlist_context();
$context->set('contextid', $contextid)
->set('contextlistid', $contextlistid)
$contexts = [];
}
+ if ($request->get('type') == static::DATAREQUEST_TYPE_DELETE) {
+ $context = \context::instance_by_id($record->contextid);
+ $purpose = static::get_effective_context_purpose($context);
+ // Data can only be deleted from it if the context is either expired, or unprotected.
+ if (!expired_contexts_manager::is_context_expired_or_unprotected_for_user($context, $foruser)) {
+ continue;
+ }
+ }
+
$contexts[] = $record->contextid;
$lastcomponent = $record->component;
}
return true;
}
+
+ /**
+ * Format the supplied date interval as a retention period.
+ *
+ * @param \DateInterval $interval
+ * @return string
+ */
+ public static function format_retention_period(\DateInterval $interval) : string {
+ // It is one or another.
+ if ($interval->y) {
+ $formattedtime = get_string('numyears', 'moodle', $interval->format('%y'));
+ } else if ($interval->m) {
+ $formattedtime = get_string('nummonths', 'moodle', $interval->format('%m'));
+ } else if ($interval->d) {
+ $formattedtime = get_string('numdays', 'moodle', $interval->format('%d'));
+ } else {
+ $formattedtime = get_string('retentionperiodzero', 'tool_dataprivacy');
+ }
+
+ return $formattedtime;
+ }
}
* @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,
],
'type' => PARAM_INT,
'description' => 'The deletion status of the context.',
- ),
- );
+ ],
+ ];
}
/**
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;
}
* @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;
+
}
/**
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'));
+ }
}
/** @var manager The privacy manager */
protected $manager = null;
+ /** @var \progress_trace Trace tool for logging */
+ protected $trace = null;
+
+ /**
+ * Constructor for the expired_contexts_manager.
+ *
+ * @param \progress_trace $trace
+ */
+ public function __construct(\progress_trace $trace = null) {
+ if (null === $trace) {
+ $trace = new \null_progress_trace();
+ }
+
+ $this->trace = $trace;
+ }
+
/**
* Flag expired contexts as expired.
*
* @return int[] The number of contexts flagged as expired for courses, and users.
*/
public function flag_expired_contexts() : array {
+ $this->trace->output('Checking requirements');
if (!$this->check_requirements()) {
+ $this->trace->output('Requirements not met. Cannot process expired retentions.', 1);
return [0, 0];
}
// Clear old and stale records first.
+ $this->trace->output('Clearing obselete records.', 0);
static::clear_old_records();
+ $this->trace->output('Done.', 1);
+ $this->trace->output('Calculating potential course expiries.', 0);
$data = static::get_nested_expiry_info_for_courses();
+
$coursecount = 0;
+ $this->trace->output('Updating course expiry data.', 0);
foreach ($data as $expiryrecord) {
if ($this->update_from_expiry_info($expiryrecord)) {
$coursecount++;
}
}
+ $this->trace->output('Done.', 1);
+ $this->trace->output('Calculating potential user expiries.', 0);
$data = static::get_nested_expiry_info_for_user();
+
$usercount = 0;
+ $this->trace->output('Updating user expiry data.', 0);
foreach ($data as $expiryrecord) {
if ($this->update_from_expiry_info($expiryrecord)) {
$usercount++;
}
}
+ $this->trace->output('Done.', 1);
return [$coursecount, $usercount];
}
$datalist = [];
$expiredcontents = [];
$pathstoskip = [];
+
+ $userpurpose = data_registry::get_effective_contextlevel_value(CONTEXT_USER, 'purpose');
foreach ($fulllist as $record) {
\context_helper::preload_from_record($record);
$context = \context::instance_by_id($record->id, false);
continue;
}
- $purposevalue = $record->purposeid !== null ? $record->purposeid : context_instance::NOTSET;
- $purpose = api::get_effective_context_purpose($context, $purposevalue);
+ if ($context instanceof \context_user) {
+ $purpose = $userpurpose;
+ } else {
+ $purposevalue = $record->purposeid !== null ? $record->purposeid : context_instance::NOTSET;
+ $purpose = api::get_effective_context_purpose($context, $purposevalue);
+ }
if ($context instanceof \context_user && !empty($record->userdeleted)) {
$expiryinfo = static::get_expiry_info($purpose, $record->userdeleted);
} else {
$expiryinfo = static::get_expiry_info($purpose, $record->expirydate);
}
+
foreach ($datalist as $path => $data) {
// Merge with already-processed children.
if (strpos($path, $context->path) !== 0) {
$expiryinfo->merge_with_child($data->info);
}
+
$datalist[$context->path] = (object) [
'context' => $context,
'record' => $record,
}));
if (!$shouldskip && $context instanceof \context_user) {
- // The context instanceid is the user's ID.
- if (isguestuser($context->instanceid) || is_siteadmin($context->instanceid)) {
- // This is an admin, or the guest and cannot be deleted.
- $shouldskip = true;
- }
-
- if (!$shouldskip) {
- $courses = enrol_get_users_courses($context->instanceid, false, ['enddate']);
- $requireenddate = self::require_all_end_dates_for_user_deletion();
-
- foreach ($courses as $course) {
- if (empty($course->enddate)) {
- // This course has no end date.
- if ($requireenddate) {
- // Course end dates are required, and this course has no end date.
- $shouldskip = true;
- break;
- }
-
- // Course end dates are not required. The subsequent checks are pointless at this time so just
- // skip them.
- continue;
- }
-
- if ($course->enddate >= time()) {
- // This course is still in the future.
- $shouldskip = true;
- break;
- }
-
- // This course has an end date which is in the past.
- if (!self::is_course_expired($course)) {
- // This course has not expired yet.
- $shouldskip = true;
- break;
- }
- }
- }
+ $shouldskip = !self::are_user_context_dependencies_expired($context);
}
if ($shouldskip) {
* @return int[] The number of deleted contexts.
*/
public function process_approved_deletions() : array {
+ $this->trace->output('Checking requirements');
if (!$this->check_requirements()) {
+ $this->trace->output('Requirements not met. Cannot process expired retentions.', 1);
return [0, 0];
}
+ $this->trace->output('Fetching all approved and expired contexts for deletion.');
$expiredcontexts = expired_context::get_records(['status' => expired_context::STATUS_APPROVED]);
+ $this->trace->output('Done.', 1);
$totalprocessed = 0;
$usercount = 0;
$coursecount = 0;
foreach ($expiredcontexts as $expiredctx) {
$context = \context::instance_by_id($expiredctx->get('contextid'), IGNORE_MISSING);
+
if (empty($context)) {
// Unable to process this request further.
// We have no context to delete.
continue;
}
+ $this->trace->output("Deleting data for " . $context->get_context_name(), 2);
if ($this->delete_expired_context($expiredctx)) {
+ $this->trace->output("Done.", 3);
if ($context instanceof \context_user) {
$usercount++;
} else {
}
$privacymanager = $this->get_privacy_manager();
- if ($context instanceof \context_user) {
- $this->delete_expired_user_context($expiredctx);
- } else {
- // This context is fully expired - that is that the default retention period has been reached.
- $privacymanager->delete_data_for_all_users_in_context($context);
+ if ($expiredctx->is_fully_expired()) {
+ if ($context instanceof \context_user) {
+ $this->delete_expired_user_context($expiredctx);
+ } else {
+ // This context is fully expired - that is that the default retention period has been reached, and there are
+ // no remaining overrides.
+ $privacymanager->delete_data_for_all_users_in_context($context);
+ }
+
+ // Mark the record as cleaned.
+ $expiredctx->set('status', expired_context::STATUS_CLEANED);
+ $expiredctx->save();
+
+ return $context;
+ }
+
+ // We need to find all users in the context, and delete just those who have expired.
+ $collection = $privacymanager->get_users_in_context($context);
+
+ // Apply the expired and unexpired filters to remove the users in these categories.
+ $userassignments = $this->get_role_users_for_expired_context($expiredctx, $context);
+ $approvedcollection = new \core_privacy\local\request\userlist_collection($context);
+ foreach ($collection as $pendinguserlist) {
+ $userlist = filtered_userlist::create_from_userlist($pendinguserlist);
+ $userlist->apply_expired_context_filters($userassignments->expired, $userassignments->unexpired);
+ if (count($userlist)) {
+ $approvedcollection->add_userlist($userlist);
+ }
+ }
+
+ if (count($approvedcollection)) {
+ // Perform the deletion with the newly approved collection.
+ $privacymanager->delete_data_for_users_in_context($approvedcollection);
}
// Mark the record as cleaned.
* @return expiry_info
*/
protected static function get_expiry_info(purpose $purpose, int $comparisondate = 0) : expiry_info {
- if (empty($comparisondate)) {
- // The date is empty, therefore this context cannot be considered for automatic expiry.
- $defaultexpired = false;
+ $overrides = $purpose->get_purpose_overrides();
+ $expiredroles = $unexpiredroles = [];
+ if (empty($overrides)) {
+ // There are no overrides for this purpose.
+ if (empty($comparisondate)) {
+ // The date is empty, therefore this context cannot be considered for automatic expiry.
+ $defaultexpired = false;
+ } else {
+ $defaultexpired = static::has_expired($purpose->get('retentionperiod'), $comparisondate);
+ }
+
+ return new expiry_info($defaultexpired, $purpose->get('protected'), [], [], []);
} else {
- $defaultexpired = static::has_expired($purpose->get('retentionperiod'), $comparisondate);
- }
+ $protectedroles = [];
+ 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');
- return new expiry_info($defaultexpired);
+ if ($override->get('protected')) {
+ $protectedroles[$override->get('roleid')] = true;
+ }
+ }
+ }
+
+ $defaultexpired = false;
+ if (static::has_expired($purpose->get('retentionperiod'), $comparisondate)) {
+ $defaultexpired = true;
+ }
+
+ if ($defaultexpired) {
+ $expiredroles = [];
+ }
+
+ return new expiry_info($defaultexpired, $purpose->get('protected'), $expiredroles, $unexpiredroles, $protectedroles);
+ }
}
/**
* @return expired_context|null
*/
protected function update_from_expiry_info(\stdClass $expiryrecord) {
- if ($expiryrecord->info->is_any_expired()) {
+ if ($isanyexpired = $expiryrecord->info->is_any_expired()) {
// The context is expired in some fashion.
// Create or update as required.
if ($expiryrecord->record->expiredctxid) {
$expiredcontext = expired_context::create_from_expiry_info($expiryrecord->context, $expiryrecord->info);
}
+ if ($expiryrecord->context instanceof \context_user) {
+ $userassignments = $this->get_role_users_for_expired_context($expiredcontext, $expiryrecord->context);
+ if (!empty($userassignments->unexpired)) {
+ $expiredcontext->delete();
+
+ return null;
+ }
+ }
+
return $expiredcontext;
} else {
// The context is not expired.
// Fetch the current nested expiry data.
$expiryrecords = self::get_nested_expiry_info($context->path);
- // Find the current record.
if (empty($expiryrecords[$context->path])) {
$expiredctx->delete();
return null;
return $expiredctx;
}
+ /**
+ * Get the list of actual users for the combination of expired, and unexpired roles.
+ *
+ * @param expired_context $expiredctx
+ * @param \context $context
+ * @return \stdClass
+ */
+ protected function get_role_users_for_expired_context(expired_context $expiredctx, \context $context) : \stdClass {
+ $expiredroles = $expiredctx->get('expiredroles');
+ $expiredroleusers = [];
+ if (!empty($expiredroles)) {
+ // Find the list of expired role users.
+ $expiredroleuserassignments = get_role_users($expiredroles, $context, true, 'ra.id, u.id AS userid', 'ra.id');
+ $expiredroleusers = array_map(function($assignment) {
+ return $assignment->userid;
+ }, $expiredroleuserassignments);
+ }
+ $expiredroleusers = array_unique($expiredroleusers);
+
+ $unexpiredroles = $expiredctx->get('unexpiredroles');
+ $unexpiredroleusers = [];
+ if (!empty($unexpiredroles)) {
+ // Find the list of unexpired role users.
+ $unexpiredroleuserassignments = get_role_users($unexpiredroles, $context, true, 'ra.id, u.id AS userid', 'ra.id');
+ $unexpiredroleusers = array_map(function($assignment) {
+ return $assignment->userid;
+ }, $unexpiredroleuserassignments);
+ }
+ $unexpiredroleusers = array_unique($unexpiredroleusers);
+
+ if (!$expiredctx->get('defaultexpired')) {
+ $tofilter = get_users_roles($context, $expiredroleusers);
+ $tofilter = array_filter($tofilter, function($userroles) use ($expiredroles) {
+ // Each iteration contains the list of role assignment for a specific user.
+ // All roles that the user holds must match those in the list of expired roles.
+ 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,
+ ];
+ }
+
+ /**
+ * Determine whether the supplied context has expired.
+ *
+ * @param \context $context
+ * @return bool
+ */
+ public static function is_context_expired(\context $context) : bool {
+ $parents = $context->get_parent_contexts(true);
+ foreach ($parents as $parent) {
+ if ($parent instanceof \context_course) {
+ return self::is_course_context_expired($context);
+ }
+
+ if ($parent instanceof \context_user) {
+ return self::are_user_context_dependencies_expired($context);
+ }
+ }
+
+ return false;
+ }
+
/**
* Check whether the course has expired.
*
*/
protected static function is_course_expired(\stdClass $course) : bool {
$context = \context_course::instance($course->id);
+
+ return self::is_course_context_expired($context);
+ }
+
+ /**
+ * Determine whether the supplied course context has expired.
+ *
+ * @param \context_course $context
+ * @return bool
+ */
+ protected static function is_course_context_expired(\context_course $context) : bool {
$expiryrecords = self::get_nested_expiry_info_for_courses($context->path);
return !empty($expiryrecords[$context->path]) && $expiryrecords[$context->path]->info->is_fully_expired();
}
+ /**
+ * Determine whether the supplied user context's dependencies have expired.
+ *
+ * This checks whether courses have expired, and some other check, but does not check whether the user themself has expired.
+ *
+ * Although this seems unusual at first, each location calling this actually checks whether the user is elgible for
+ * deletion, irrespective if they have actually expired.
+ *
+ * For example, a request to delete the user only cares about course dependencies and the user's lack of expiry
+ * should not block their own request to be deleted; whilst the expiry eligibility check has already tested for the
+ * user being expired.
+ *
+ * @param \context_user $context
+ * @return bool
+ */
+ protected static function are_user_context_dependencies_expired(\context_user $context) : bool {
+ // The context instanceid is the user's ID.
+ if (isguestuser($context->instanceid) || is_siteadmin($context->instanceid)) {
+ // This is an admin, or the guest and cannot expire.
+ return false;
+ }
+
+ $courses = enrol_get_users_courses($context->instanceid, false, ['enddate']);
+ $requireenddate = self::require_all_end_dates_for_user_deletion();
+
+ $expired = true;
+
+ foreach ($courses as $course) {
+ if (empty($course->enddate)) {
+ // This course has no end date.
+ if ($requireenddate) {
+ // Course end dates are required, and this course has no end date.
+ $expired = false;
+ break;
+ }
+
+ // Course end dates are not required. The subsequent checks are pointless at this time so just
+ // skip them.
+ continue;
+ }
+
+ if ($course->enddate >= time()) {
+ // This course is still in the future.
+ $expired = false;
+ break;
+ }
+
+ // This course has an end date which is in the past.
+ if (!self::is_course_expired($course)) {
+ // This course has not expired yet.
+ $expired = false;
+ break;
+ }
+ }
+
+ return $expired;
+ }
+
+ /**
+ * Determine whether the supplied context has expired or unprotected for the specified user.
+ *
+ * @param \context $context
+ * @param \stdClass $user
+ * @return bool
+ */
+ public static function is_context_expired_or_unprotected_for_user(\context $context, \stdClass $user) : bool {
+ $parents = $context->get_parent_contexts(true);
+ foreach ($parents as $parent) {
+ if ($parent instanceof \context_course) {
+ return self::is_course_context_expired_or_unprotected_for_user($parent, $user);
+ }
+
+ if ($parent instanceof \context_user) {
+ return self::are_user_context_dependencies_expired($context);
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Determine whether the supplied course context has expired, or is unprotected.
+ *
+ * @param \context_course $context
+ * @param \stdClass $user
+ * @return bool
+ */
+ protected static function is_course_context_expired_or_unprotected_for_user(\context_course $context, \stdClass $user) {
+ $expiryrecords = self::get_nested_expiry_info_for_courses($context->path);
+
+ $info = $expiryrecords[$context->path]->info;
+ if ($info->is_fully_expired()) {
+ // This context is fully expired.
+ return true;
+ }
+
+ // Now perform user checks.
+ $userroles = array_map(function($assignment) {
+ return $assignment->roleid;
+ }, get_user_roles($context, $user->id));
+
+ $unexpiredprotectedroles = $info->get_unexpired_protected_roles();
+ if (!empty(array_intersect($unexpiredprotectedroles, $userroles))) {
+ // The user holds an unexpired and protected role.
+ return false;
+ }
+
+ $unprotectedoverriddenroles = $info->get_unprotected_overridden_roles();
+ $matchingroles = array_intersect($unprotectedoverriddenroles, $userroles);
+ if (!empty($matchingroles)) {
+ // This user has at least one overridden role which is not a protected.
+ // However, All such roles must match.
+ // If the user has multiple roles then all must be expired, otherwise we should fall back to the default behaviour.
+ if (empty(array_diff($userroles, $unprotectedoverriddenroles))) {
+ // All roles that this user holds are a combination of expired, or unprotected.
+ return true;
+ }
+ }
+
+ if ($info->is_default_expired()) {
+ // If the user has no unexpired roles, and the context is expired by default then this must be expired.
+ return true;
+ }
+
+ return !$info->is_default_protected();
+ }
+
/**
* Create a new instance of the privacy manager.
*
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 bool Whether the default purpose is protected */
+ protected $defaultprotected = false;
+
+ /** @var int[] List of expires roles */
+ protected $expired = [];
+
+ /** @var int[] List of unexpires roles */
+ protected $unexpired = [];
+
+ /** @var int[] List of unexpired roles which are also protected */
+ protected $protectedroles = [];
/**
* 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 bool $defaultprotected Whether the default expiry is protected.
+ * @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.
+ * @param int[] $protectedroles A list of unexpired roles in this context which are protected.
*/
- public function __construct(bool $isexpired) {
- $this->isexpired = $isexpired;
+ public function __construct(bool $default, bool $defaultprotected, array $expired, array $unexpired, array $protectedroles) {
+ $this->defaultexpiryreached = $default;
+ $this->defaultprotected = $defaultprotected;
+ $this->expired = $expired;
+ $this->unexpired = $unexpired;
+ $this->protectedroles = $protectedroles;
}
/**
* @return bool
*/
public function is_fully_expired() : bool {
- return $this->isexpired;
+ return $this->defaultexpiryreached && empty($this->unexpired);
}
/**
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;
+ }
+
+ /**
+ * Whether the default purpose is protected.
+ *
+ * @return bool
+ */
+ public function is_default_protected() : bool {
+ return $this->defaultprotected;
+ }
+
+ /**
+ * Get the list of unexpired role IDs.
+ *
+ * @return int[]
+ */
+ public function get_unexpired_roles() : array {
+ return $this->unexpired;
+ }
+
+ /**
+ * Get the list of unexpired protected roles.
+ *
+ * @return int[]
+ */
+ public function get_unexpired_protected_roles() : array {
+ return array_keys(array_filter($this->protectedroles));
+ }
+
+ /**
+ * Get a list of all overridden roles which are unprotected.
+ * @return int[]
+ */
+ public function get_unprotected_overridden_roles() : array {
+ $allroles = array_merge($this->expired, $this->unexpired);
+
+ return array_diff($allroles, $this->protectedroles);
+ }
+
/**
* Merge this expiry_info object with another belonging to a child context in order to set the 'safest' heritage.
*
}
// 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;
}
use coding_exception;
use core\external\persistent_exporter;
-use DateInterval;
use Exception;
use renderer_base;
use tool_dataprivacy\context_instance;
'multiple' => true,
'optional' => true
],
+ 'roleoverrides' => [
+ 'type' => PARAM_TEXT
+ ],
];
}
$retentionperiod = $this->persistent->get('retentionperiod');
if ($retentionperiod) {
- $interval = new DateInterval($retentionperiod);
-
- // It is one or another.
- if ($interval->y) {
- $formattedtime = get_string('numyears', 'moodle', $interval->format('%y'));
- } else if ($interval->m) {
- $formattedtime = get_string('nummonths', 'moodle', $interval->format('%m'));
- } else if ($interval->d) {
- $formattedtime = get_string('numdays', 'moodle', $interval->format('%d'));
- } else {
- $formattedtime = get_string('retentionperiodzero', 'tool_dataprivacy');
- }
+ $formattedtime = \tool_dataprivacy\api::format_retention_period(new \DateInterval($retentionperiod));
} else {
$formattedtime = get_string('retentionperiodnotdefined', 'tool_dataprivacy');
}
$values['formattedretentionperiod'] = $formattedtime;
+ $values['roleoverrides'] = !empty($this->persistent->get_purpose_overrides());
+
return $values;
}
--- /dev/null
+<?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 <andrew@nicols.co.uk>
+ * @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 <andrew@nicols.co.uk>
+ * @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;
+ }
+}
*/
protected static $persistentclass = 'tool_dataprivacy\\purpose';
+ /**
+ * @var array The list of current overrides.
+ */
+ protected $existingoverrides = [];
+
/**
* Define the form - called by parent constructor
*/
$mform->setType('description', PARAM_CLEANHTML);
// Field for selecting lawful bases (from GDPR Article 6.1).
- $lawfulbases = [];
+ $this->add_field($this->get_lawful_base_field());
+ $mform->addRule('lawfulbases', get_string('required'), 'required', null, 'server');
+
+ // Optional field for selecting reasons for collecting sensitive personal data (from GDPR Article 9.2).
+ $this->add_field($this->get_sensitive_base_field());
+
+ $this->add_field($this->get_retention_period_fields());
+ $this->add_field($this->get_protected_field());
+
+ $this->add_override_fields();
+
+ if (!empty($this->_customdata['showbuttons'])) {
+ if (!$this->get_persistent()->get('id')) {
+ $savetext = get_string('add');
+ } else {
+ $savetext = get_string('savechanges');
+ }
+ $this->add_action_buttons(true, $savetext);
+ }
+ }
+
+ /**
+ * Add a fieldset to the current form.
+ *
+ * @param \stdClass $data
+ */
+ protected function add_field(\stdClass $data) {
+ foreach ($data->fields as $field) {
+ $this->_form->addElement($field);
+ }
+
+ if (!empty($data->helps)) {
+ foreach ($data->helps as $fieldname => $helpdata) {
+ $help = array_merge([$fieldname], $helpdata);
+ call_user_func_array([$this->_form, 'addHelpButton'], $help);
+ }
+ }
+
+ if (!empty($data->types)) {
+ foreach ($data->types as $fieldname => $type) {
+ $this->_form->setType($fieldname, $type);
+ }
+ }
+
+ if (!empty($data->rules)) {
+ foreach ($data->rules as $fieldname => $ruledata) {
+ $rule = array_merge([$fieldname], $ruledata);
+ call_user_func_array([$this->_form, 'addRule'], $rule);
+ }
+ }
+
+ if (!empty($data->defaults)) {
+ foreach ($data->defaults as $fieldname => $default) {
+ $this->_form($fieldname, $default);
+ }
+ }
+ }
+
+ /**
+ * Handle addition of relevant repeated element fields for role overrides.
+ */
+ protected function add_override_fields() {
+ $purpose = $this->get_persistent();
+
+ if (empty($purpose->get('id'))) {
+ // It is not possible to use repeated elements in a modal form yet.
+ return;
+ }
+
+ $fields = [
+ $this->get_role_override_id('roleoverride_'),
+ $this->get_role_field('roleoverride_'),
+ $this->get_retention_period_fields('roleoverride_'),
+ $this->get_protected_field('roleoverride_'),
+ $this->get_lawful_base_field('roleoverride_'),
+ $this->get_sensitive_base_field('roleoverride_'),
+ ];
+
+ $options = [
+ 'type' => [],
+ 'helpbutton' => [],
+ ];
+
+ // Start by adding the title.
+ $overrideelements = [
+ $this->_form->createElement('header', 'roleoverride', get_string('roleoverride', 'tool_dataprivacy')),
+ $this->_form->createElement(
+ 'static',
+ 'roleoverrideoverview',
+ '',
+ get_string('roleoverrideoverview', 'tool_dataprivacy')
+ ),
+ ];
+
+ foreach ($fields as $fielddata) {
+ foreach ($fielddata->fields as $field) {
+ $overrideelements[] = $field;
+ }
+
+ if (!empty($fielddata->helps)) {
+ foreach ($fielddata->helps as $name => $help) {
+ if (!isset($options[$name])) {
+ $options[$name] = [];
+ }
+ $options[$name]['helpbutton'] = $help;
+ }
+ }
+
+ if (!empty($fielddata->types)) {
+ foreach ($fielddata->types as $name => $type) {
+ if (!isset($options[$name])) {
+ $options[$name] = [];
+ }
+ $options[$name]['type'] = $type;
+ }
+ }
+
+ if (!empty($fielddata->rules)) {
+ foreach ($fielddata->rules as $name => $rule) {
+ if (!isset($options[$name])) {
+ $options[$name] = [];
+ }
+ $options[$name]['rule'] = $rule;
+ }
+ }
+
+ if (!empty($fielddata->defaults)) {
+ foreach ($fielddata->defaults as $name => $default) {
+ if (!isset($options[$name])) {
+ $options[$name] = [];
+ }
+ $options[$name]['default'] = $default;
+ }
+ }
+
+ if (!empty($fielddata->advanceds)) {
+ foreach ($fielddata->advanceds as $name => $advanced) {
+ if (!isset($options[$name])) {
+ $options[$name] = [];
+ }
+ $options[$name]['advanced'] = $advanced;
+ }
+ }
+ }
+
+ $this->existingoverrides = $purpose->get_purpose_overrides();
+ $existingoverridecount = count($this->existingoverrides);
+
+ $this->repeat_elements(
+ $overrideelements,
+ $existingoverridecount,
+ $options,
+ 'overrides',
+ 'addoverride',
+ 1,
+ get_string('addroleoverride', 'tool_dataprivacy')
+ );
+ }
+
+ /**
+ * Converts fields.
+ *
+ * @param \stdClass $data
+ * @return \stdClass
+ */
+ public function filter_data_for_persistent($data) {
+ $data = parent::filter_data_for_persistent($data);
+
+ $classname = static::$persistentclass;
+ $properties = $classname::properties_definition();
+
+ $data = (object) array_filter((array) $data, function($value, $key) use ($properties) {
+ return isset($properties[$key]);
+ }, ARRAY_FILTER_USE_BOTH);
+
+ return $data;
+ }
+
+ /**
+ * Get the field for the role name.
+ *
+ * @param string $prefix The prefix to apply to the field
+ * @return \stdClass
+ */
+ protected function get_role_override_id(string $prefix = '') : \stdClass {
+ $fieldname = "{$prefix}id";
+
+ $fielddata = (object) [
+ 'fields' => [],
+ ];
+
+ $fielddata->fields[] = $this->_form->createElement('hidden', $fieldname);
+ $fielddata->types[$fieldname] = PARAM_INT;
+
+ return $fielddata;
+ }
+
+ /**
+ * Get the field for the role name.
+ *
+ * @param string $prefix The prefix to apply to the field
+ * @return \stdClass
+ */
+ protected function get_role_field(string $prefix = '') : \stdClass {
+ $fieldname = "{$prefix}roleid";
+
+ $fielddata = (object) [
+ 'fields' => [],
+ 'helps' => [],
+ ];
+
+ $roles = [
+ '' => get_string('none'),
+ ];
+ foreach (role_get_names() as $roleid => $role) {
+ $roles[$roleid] = $role->localname;
+ }
+
+ $fielddata->fields[] = $this->_form->createElement('select', $fieldname, get_string('role'),
+ $roles,
+ [
+ 'multiple' => false,
+ ]
+ );
+ $fielddata->helps[$fieldname] = ['role', 'tool_dataprivacy'];
+ $fielddata->defaults[$fieldname] = null;
+
+ return $fielddata;
+ }
+
+ /**
+ * Get the mform field for lawful bases.
+ *
+ * @param string $prefix The prefix to apply to the field
+ * @return \stdClass
+ */
+ protected function get_lawful_base_field(string $prefix = '') : \stdClass {
+ $fieldname = "{$prefix}lawfulbases";
+
+ $data = (object) [
+ 'fields' => [],
+ ];
+
+ $bases = [];
foreach (\tool_dataprivacy\purpose::GDPR_ART_6_1_ITEMS as $article) {
$key = 'gdpr_art_6_1_' . $article;
- $lawfulbases[$key] = get_string($key . '_name', 'tool_dataprivacy');
+ $bases[$key] = get_string("{$key}_name", 'tool_dataprivacy');
}
- $options = array(
- 'multiple' => true,
+
+ $data->fields[] = $this->_form->createElement('autocomplete', $fieldname, get_string('lawfulbases', 'tool_dataprivacy'),
+ $bases,
+ [
+ 'multiple' => true,
+ ]
);
- $mform->addElement('autocomplete', 'lawfulbases', get_string('lawfulbases', 'tool_dataprivacy'), $lawfulbases, $options);
- $mform->addRule('lawfulbases', get_string('required'), 'required', null, 'server');
- $mform->addHelpButton('lawfulbases', 'lawfulbases', 'tool_dataprivacy');
- // Optional field for selecting reasons for collecting sensitive personal data (from GDPR Article 9.2).
- $sensitivereasons = [];
+ $data->helps = [
+ $fieldname => ['lawfulbases', 'tool_dataprivacy'],
+ ];
+
+ $data->advanceds = [
+ $fieldname => true,
+ ];
+
+ return $data;
+ }
+
+ /**
+ * Get the mform field for sensitive bases.
+ *
+ * @param string $prefix The prefix to apply to the field
+ * @return \stdClass
+ */
+ protected function get_sensitive_base_field(string $prefix = '') : \stdClass {
+ $fieldname = "{$prefix}sensitivedatareasons";
+
+ $data = (object) [
+ 'fields' => [],
+ ];
+
+ $bases = [];
foreach (\tool_dataprivacy\purpose::GDPR_ART_9_2_ITEMS as $article) {
$key = 'gdpr_art_9_2_' . $article;
- $sensitivereasons[$key] = get_string($key . '_name', 'tool_dataprivacy');
+ $bases[$key] = get_string("{$key}_name", 'tool_dataprivacy');
}
- $mform->addElement('autocomplete', 'sensitivedatareasons', get_string('sensitivedatareasons', 'tool_dataprivacy'),
- $sensitivereasons, $options);
- $mform->addHelpButton('sensitivedatareasons', 'sensitivedatareasons', 'tool_dataprivacy');
- $number = $mform->createElement('text', 'retentionperiodnumber', null, ['size' => 8]);
+ $data->fields[] = $this->_form->createElement(
+ 'autocomplete',
+ $fieldname,
+ get_string('sensitivedatareasons', 'tool_dataprivacy'),
+ $bases,
+ [
+ 'multiple' => true,
+ ]
+ );
+ $data->helps = [
+ $fieldname => ['sensitivedatareasons', 'tool_dataprivacy'],
+ ];
+
+ $data->advanceds = [
+ $fieldname => true,
+ ];
+
+ return $data;
+ }
+
+ /**
+ * Get the retention period fields.
+ *
+ * @param string $prefix The name of the main field, and prefix for the subfields.
+ * @return \stdClass
+ */
+ protected function get_retention_period_fields(string $prefix = '') : \stdClass {
+ $prefix = "{$prefix}retentionperiod";
+ $data = (object) [
+ 'fields' => [],
+ 'types' => [],
+ ];
+
+ $number = $this->_form->createElement('text', "{$prefix}number", null, ['size' => 8]);
+ $data->types["{$prefix}number"] = PARAM_INT;
+
$unitoptions = [
'Y' => get_string('years'),
'M' => strtolower(get_string('months')),
'D' => strtolower(get_string('days'))
];
- $unit = $mform->createElement('select', 'retentionperiodunit', '', $unitoptions);
- $mform->addGroup(['number' => $number, 'unit' => $unit], 'retentionperiod',
- get_string('retentionperiod', 'tool_dataprivacy'), null, false);
- $mform->setType('retentionperiodnumber', PARAM_INT);
+ $unit = $this->_form->createElement('select', "{$prefix}unit", '', $unitoptions);
- $this->_form->addElement('advcheckbox', 'protected', get_string('protected', 'tool_dataprivacy'),
- get_string('protectedlabel', 'tool_dataprivacy'));
+ $data->fields[] = $this->_form->createElement(
+ 'group',
+ $prefix,
+ get_string('retentionperiod', 'tool_dataprivacy'),
+ [
+ 'number' => $number,
+ 'unit' => $unit,
+ ],
+ null,
+ false
+ );
- if (!empty($this->_customdata['showbuttons'])) {
- if (!$this->get_persistent()->get('id')) {
- $savetext = get_string('add');
- } else {
- $savetext = get_string('savechanges');
- }
- $this->add_action_buttons(true, $savetext);
- }
+ return $data;
}
/**
- * Converts fields.
+ * Get the mform field for the protected flag.
+ *
+ * @param string $prefix The prefix to apply to the field
+ * @return \stdClass
+ */
+ protected function get_protected_field(string $prefix = '') : \stdClass {
+ $fieldname = "{$prefix}protected";
+
+ return (object) [
+ 'fields' => [
+ $this->_form->createElement(
+ 'advcheckbox',
+ $fieldname,
+ get_string('protected', 'tool_dataprivacy'),
+ get_string('protectedlabel', 'tool_dataprivacy')
+ ),
+ ],
+ ];
+ }
+
+ /**
+ * Converts data to data suitable for storage.
*
* @param \stdClass $data
* @return \stdClass
protected static function convert_fields(\stdClass $data) {
$data = parent::convert_fields($data);
- if (is_array($data->lawfulbases)) {
+ if (!empty($data->lawfulbases) && is_array($data->lawfulbases)) {
$data->lawfulbases = implode(',', $data->lawfulbases);
}
if (!empty($data->sensitivedatareasons) && is_array($data->sensitivedatareasons)) {
$data->retentionperiod = 'P' . $data->retentionperiodnumber . $data->retentionperiodunit;
unset($data->retentionperiodnumber);
unset($data->retentionperiodunit);
+
return $data;
}
protected function get_default_data() {
$data = parent::get_default_data();
+ return $this->convert_existing_data_to_values($data);
+ }
+
+ /**
+ * Normalise any values stored in existing data.
+ *
+ * @param \stdClass $data
+ * @return \stdClass
+ */
+ protected function convert_existing_data_to_values(\stdClass $data) : \stdClass {
$data->lawfulbases = explode(',', $data->lawfulbases);
if (!empty($data->sensitivedatareasons)) {
$data->sensitivedatareasons = explode(',', $data->sensitivedatareasons);
return $data;
}
+
+ /**
+ * Fetch the role override data from the list of submitted data.
+ *
+ * @param \stdClass $data The complete set of processed data
+ * @return \stdClass[] The list of overrides
+ */
+ public function get_role_overrides_from_data(\stdClass $data) {
+ $overrides = [];
+ if (!empty($data->overrides)) {
+ $searchkey = 'roleoverride_';
+
+ for ($i = 0; $i < $data->overrides; $i++) {
+ $overridedata = (object) [];
+ foreach ((array) $data as $fieldname => $value) {
+ if (strpos($fieldname, $searchkey) !== 0) {
+ continue;
+ }
+
+ $overridefieldname = substr($fieldname, strlen($searchkey));
+ $overridedata->$overridefieldname = $value[$i];
+ }
+
+ if (empty($overridedata->roleid) || empty($overridedata->retentionperiodnumber)) {
+ // Skip this one.
+ // There is no value and it will be delete.
+ continue;
+ }
+
+ $override = static::convert_fields($overridedata);
+
+ $overrides[$i] = $override;
+ }
+ }
+
+ return $overrides;
+ }
+
+ /**
+ * Define extra validation mechanims.
+ *
+ * @param stdClass $data Data to validate.
+ * @param array $files Array of files.
+ * @param array $errors Currently reported errors.
+ * @return array of additional errors, or overridden errors.
+ */
+ protected function extra_validation($data, $files, array &$errors) {
+ $overrides = $this->get_role_overrides_from_data($data);
+
+ // Check role overrides to ensure that:
+ // - roles are unique; and
+ // - specifeid retention periods are numeric.
+ $seenroleids = [];
+ foreach ($overrides as $id => $override) {
+ $override->purposeid = 0;
+ $persistent = new \tool_dataprivacy\purpose_override($override->id, $override);
+
+ if (isset($seenroleids[$persistent->get('roleid')])) {
+ $errors["roleoverride_roleid[{$id}]"] = get_string('duplicaterole');
+ }
+ $seenroleids[$persistent->get('roleid')] = true;
+
+ $errors = array_merge($errors, $persistent->get_errors());
+ }
+
+ return $errors;
+ }
+
+ /**
+ * Load in existing data as form defaults. Usually new entry defaults are stored directly in
+ * form definition (new entry form); this function is used to load in data where values
+ * already exist and data is being edited (edit entry form).
+ *
+ * @param stdClass $data
+ */
+ public function set_data($data) {
+ $purpose = $this->get_persistent();
+
+ $count = 0;
+ foreach ($this->existingoverrides as $override) {
+ $overridedata = $this->convert_existing_data_to_values($override->to_record());
+ foreach ($overridedata as $key => $value) {
+ $keyname = "roleoverride_{$key}[{$count}]";
+ $data->$keyname = $value;
+ }
+ $count++;
+ }
+
+ parent::set_data($data);
+ }
}
*/
protected $selectall = true;
- /** @var purpose[] Array of purposes mapped to the contexts. */
+ /** @var purpose[] Array of purposes by their id. */
protected $purposes = [];
+ /** @var purpose[] Map of context => purpose. */
+ protected $purposemap = [];
+
+ /** @var array List of roles. */
+ protected $roles = [];
+
/**
* expired_contexts_table constructor.
*
'purpose' => get_string('purpose', 'tool_dataprivacy'),
'category' => get_string('category', 'tool_dataprivacy'),
'retentionperiod' => get_string('retentionperiod', 'tool_dataprivacy'),
+ 'tobedeleted' => get_string('tobedeleted', 'tool_dataprivacy'),
'timecreated' => get_string('expiry', 'tool_dataprivacy'),
];
$checkboxattrs = [
$this->no_sorting('purpose');
$this->no_sorting('category');
$this->no_sorting('retentionperiod');
+ $this->no_sorting('tobedeleted');
// Make this table sorted by first name by default.
$this->sortable(true, 'timecreated');
+
+ // We use roles in several places.
+ $this->roles = role_get_names();
}
/**
* The context name column.
*
- * @param stdClass $data The row data.
+ * @param stdClass $expiredctx The row data.
* @return string
* @throws coding_exception
*/
- public function col_name($data) {
+ public function col_name($expiredctx) {
global $OUTPUT;
- $context = context_helper::instance_by_id($data->contextid);
+ $context = context_helper::instance_by_id($expiredctx->get('contextid'));
$parent = $context->get_parent_context();
$contextdata = (object)[
'name' => $context->get_context_name(false, true),
/**
* The context information column.
*
- * @param stdClass $data The row data.
+ * @param stdClass $expiredctx The row data.
* @return string
* @throws coding_exception
*/
- public function col_info($data) {
+ public function col_info($expiredctx) {
global $OUTPUT;
- $context = context_helper::instance_by_id($data->contextid);
+ $context = context_helper::instance_by_id($expiredctx->get('contextid'));
$children = $context->get_child_contexts();
if (empty($children)) {
/**
* The category name column.
*
- * @param stdClass $data The row data.
+ * @param stdClass $expiredctx The row data.
* @return mixed
* @throws coding_exception
* @throws dml_exception
*/
- public function col_category($data) {
- $context = context_helper::instance_by_id($data->contextid);
+ public function col_category($expiredctx) {
+ $context = context_helper::instance_by_id($expiredctx->get('contextid'));
$category = api::get_effective_context_category($context);
return s($category->get('name'));
/**
* The purpose column.
*
- * @param stdClass $data The row data.
+ * @param stdClass $expiredctx The row data.
* @return string
* @throws coding_exception
*/
- public function col_purpose($data) {
- $purpose = $this->purposes[$data->contextid];
+ public function col_purpose($expiredctx) {
+ $purpose = $this->get_purpose_for_expiry($expiredctx);
return s($purpose->get('name'));
}
/**
* The retention period column.
*
- * @param stdClass $data The row data.
+ * @param stdClass $expiredctx The row data.
* @return string
- * @throws Exception
*/
- public function col_retentionperiod($data) {
- global $PAGE;
+ public function col_retentionperiod($expiredctx) {
+ $purpose = $this->get_purpose_for_expiry($expiredctx);
+
+ $expiries = [];
+
+ $expiry = html_writer::tag('dt', get_string('default'), ['class' => 'col-sm-3']);
+ if ($expiredctx->get('defaultexpired')) {
+ $expiries[get_string('default')] = get_string('expiredrolewithretention', 'tool_dataprivacy', (object) [
+ 'retention' => api::format_retention_period(new \DateInterval($purpose->get('retentionperiod'))),
+ ]);
+ } else {
+ $expiries[get_string('default')] = get_string('unexpiredrolewithretention', 'tool_dataprivacy', (object) [
+ 'retention' => api::format_retention_period(new \DateInterval($purpose->get('retentionperiod'))),
+ ]);
+ }
+
+ if (!$expiredctx->is_fully_expired()) {
+ $purposeoverrides = $purpose->get_purpose_overrides();
+
+ foreach ($expiredctx->get('unexpiredroles') as $roleid) {
+ $role = $this->roles[$roleid];
+ $override = $purposeoverrides[$roleid];
+
+ $expiries[$role->localname] = get_string('unexpiredrolewithretention', 'tool_dataprivacy', (object) [
+ 'retention' => api::format_retention_period(new \DateInterval($override->get('retentionperiod'))),
+ ]);
+ }
+
+ foreach ($expiredctx->get('expiredroles') as $roleid) {
+ $role = $this->roles[$roleid];
+ $override = $purposeoverrides[$roleid];
+
+ $expiries[$role->localname] = get_string('expiredrolewithretention', 'tool_dataprivacy', (object) [
+ 'retention' => api::format_retention_period(new \DateInterval($override->get('retentionperiod'))),
+ ]);
+ }
+ }
- $purpose = $this->purposes[$data->contextid];
+ $output = array_map(function($rolename, $expiry) {
+ $return = html_writer::tag('dt', $rolename, ['class' => 'col-sm-3']);
+ $return .= html_writer::tag('dd', $expiry, ['class' => 'col-sm-9']);
- $exporter = new purpose_exporter($purpose, ['context' => \context_system::instance()]);
- $exportedpurpose = $exporter->export($PAGE->get_renderer('core'));
+ return $return;
+ }, array_keys($expiries), $expiries);
- return $exportedpurpose->formattedretentionperiod;
+ return html_writer::tag('dl', implode($output), ['class' => 'row']);
}
/**
* The timecreated a.k.a. the context expiry date column.
*
- * @param stdClass $data The row data.
+ * @param stdClass $expiredctx The row data.
* @return string
*/
- public function col_timecreated($data) {
- return userdate($data->timecreated);
+ public function col_timecreated($expiredctx) {
+ return userdate($expiredctx->get('timecreated'));
}
/**
* Generate the select column.
*
- * @param stdClass $data The row data.
+ * @param stdClass $expiredctx The row data.
* @return string
*/
- public function col_select($data) {
- $id = $data->id;
+ public function col_select($expiredctx) {
+ $id = $expiredctx->get('id');
return html_writer::checkbox('expiredcontext_' . $id, $id, $this->selectall, '', ['class' => 'selectcontext']);
}
+ /**
+ * Formatting for the 'tobedeleted' column which indicates in a friendlier fashion whose data will be removed.
+ *
+ * @param stdClass $expiredctx The row data.
+ * @return string
+ */
+ public function col_tobedeleted($expiredctx) {
+ if ($expiredctx->is_fully_expired()) {
+ return get_string('defaultexpired', 'tool_dataprivacy');
+ }
+
+ $purpose = $this->get_purpose_for_expiry($expiredctx);
+
+ $a = (object) [];
+
+ $expiredroles = [];
+ foreach ($expiredctx->get('expiredroles') as $roleid) {
+ $expiredroles[] = html_writer::tag('li', $this->roles[$roleid]->localname);
+ }
+ $a->expired = html_writer::tag('ul', implode($expiredroles));
+
+ $unexpiredroles = [];
+ foreach ($expiredctx->get('unexpiredroles') as $roleid) {
+ $unexpiredroles[] = html_writer::tag('li', $this->roles[$roleid]->localname);
+ }
+ $a->unexpired = html_writer::tag('ul', implode($unexpiredroles));
+
+ if ($expiredctx->get('defaultexpired')) {
+ return get_string('defaultexpiredexcept', 'tool_dataprivacy', $a);
+ } else if (empty($unexpiredroles)) {
+ return get_string('defaultunexpired', 'tool_dataprivacy', $a);
+ } else {
+ return get_string('defaultunexpiredwithexceptions', 'tool_dataprivacy', $a);
+ }
+ }
+
/**
* Query the database for results to display in the table.
*
// Only load expired contexts that are awaiting confirmation.
$expiredcontexts = expired_context::get_records_by_contextlevel($this->contextlevel, expired_context::STATUS_EXPIRED,
$sort, $this->get_page_start(), $this->get_page_size());
+
$this->rawdata = [];
+ $contextids = [];
foreach ($expiredcontexts as $persistent) {
- $data = $persistent->to_record();
-
- $context = context_helper::instance_by_id($data->contextid);
-
- $purpose = api::get_effective_context_purpose($context);
- $this->purposes[$data->contextid] = $purpose;
- $this->rawdata[] = $data;
+ $this->rawdata[] = $persistent;
+ $contextids[] = $persistent->get('contextid');
}
+ $this->preload_contexts($contextids);
+
// Set initial bars.
if ($useinitialsbar) {
$this->initialbars($total > $pagesize);
}
return '';
}
+
+ /**
+ * Get the purpose for the specified expired context.
+ *
+ * @param expired_context $expiredcontext
+ * @return purpose
+ */
+ protected function get_purpose_for_expiry(expired_context $expiredcontext) : purpose {
+ $context = context_helper::instance_by_id($expiredcontext->get('contextid'));
+
+ if (empty($this->purposemap[$context->id])) {
+ $purpose = api::get_effective_context_purpose($context);
+ $this->purposemap[$context->id] = $purpose->get('id');
+
+ if (empty($this->purposes[$purpose->get('id')])) {
+ $this->purposes[$purpose->get('id')] = $purpose;
+ }
+ }
+
+ return $this->purposes[$this->purposemap[$context->id]];
+ }
+
+ /**
+ * Preload context records given a set of contextids.
+ *
+ * @param array $contextids
+ */
+ protected function preload_contexts(array $contextids) {
+ global $DB;
+
+ if (empty($contextids)) {
+ return;
+ }
+
+ $ctxfields = \context_helper::get_preload_record_columns_sql('ctx');
+ list($insql, $inparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED);
+ $sql = "SELECT {$ctxfields} FROM {context} ctx WHERE ctx.id {$insql}";
+ $contextlist = $DB->get_recordset_sql($sql, $inparams);
+ foreach ($contextlist as $contextdata) {
+ \context_helper::preload_from_record($contextdata);
+ }
+ $contextlist->close();
+
+ }
}
use context_user;
use core_privacy\local\metadata\collection;
use core_privacy\local\request\approved_contextlist;
+use \core_privacy\local\request\approved_userlist;
use core_privacy\local\request\contextlist;
use core_privacy\local\request\helper;
use core_privacy\local\request\transform;
+use \core_privacy\local\request\userlist;
use core_privacy\local\request\writer;
use dml_exception;
use stdClass;
// This tool stores user data.
\core_privacy\local\metadata\provider,
+ // This plugin is capable of determining which users have data within it.
+ \core_privacy\local\request\core_userlist_provider,
+
// This tool may provide access to and deletion of user data.
\core_privacy\local\request\plugin\provider,
return $contextlist;
}
+ /**
+ * Get the list of users who have data within a context.
+ *
+ * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
+ *
+ */
+ public static function get_users_in_context(userlist $userlist) {
+ $context = $userlist->get_context();
+
+ if (!is_a($context, \context_user::class)) {
+ return;
+ }
+
+ $params = [
+ 'contextlevel' => CONTEXT_USER,
+ 'contextid' => $context->id,
+ ];
+
+ $sql = "SELECT instanceid AS userid
+ FROM {context}
+ WHERE id = :contextid
+ AND contextlevel = :contextlevel";
+
+ $userlist->add_from_sql('userid', $sql, $params);
+ }
+
/**
* Export all user data for the specified user, in the specified contexts.
*
public static function delete_data_for_user(approved_contextlist $contextlist) {
}
+ /**
+ * Delete multiple users within a single context.
+ *
+ * @param approved_userlist $userlist The approved context and user information to delete information for.
+ *
+ */
+ public static function delete_data_for_users(approved_userlist $userlist) {
+ }
+
/**
* Export all user preferences for the plugin.
*
* @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;
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);
+ }
}
--- /dev/null
+<?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 <andrew@nicols.co.uk>
+ * @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 <andrew@nicols.co.uk>
+ * @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 {
+ $cache = \cache::make('tool_dataprivacy', 'purpose_overrides');
+
+ $overrides = [];
+ $alldata = $cache->get($purpose->get('id'));
+ if (false === $alldata) {
+ $tocache = [];
+ foreach (self::get_records(['purposeid' => $purpose->get('id')]) as $override) {
+ $tocache[] = $override->to_record();
+ $overrides[$override->get('roleid')] = $override;
+ }
+ $cache->set($purpose->get('id'), $tocache);
+ } else {
+ foreach ($alldata as $data) {
+ $override = new self(0, $data);
+ $overrides[$override->get('roleid')] = $override;
+ }
+ }
+
+ return $overrides;
+ }
+
+ /**
+ * Adds the new record to the cache.
+ *
+ * @return null
+ */
+ protected function after_create() {
+ $cache = \cache::make('tool_dataprivacy', 'purpose_overrides');
+ $cache->delete($this->get('purposeid'));
+ }
+
+ /**
+ * Updates the cache record.
+ *
+ * @param bool $result
+ * @return null
+ */
+ protected function after_update($result) {
+ $cache = \cache::make('tool_dataprivacy', 'purpose_overrides');
+ $cache->delete($this->get('purposeid'));
+ }
+
+ /**
+ * Removes unnecessary stuff from db.
+ *
+ * @return null
+ */
+ protected function before_delete() {
+ $cache = \cache::make('tool_dataprivacy', 'purpose_overrides');
+ $cache->delete($this->get('purposeid'));
+ }
+}
* Run the task to delete context instances based on their retention periods.
*/
public function execute() {
- $manager = new \tool_dataprivacy\expired_contexts_manager();
+ $manager = new \tool_dataprivacy\expired_contexts_manager(new \text_progress_trace());
list($courses, $users) = $manager->process_approved_deletions();
mtrace("Processed deletions for {$courses} course contexts, and {$users} user contexts as expired");
}
* Run the task to flag context instances as expired.
*/
public function execute() {
- $manager = new \tool_dataprivacy\expired_contexts_manager();
+ $manager = new \tool_dataprivacy\expired_contexts_manager(new \text_progress_trace());
list($courses, $users) = $manager->flag_expired_contexts();
mtrace("Flagged {$courses} course contexts, and {$users} user contexts as expired");
}
'staticacceleration' => true,
'staticaccelerationsize' => 30,
),
+ 'purpose_overrides' => array(
+ 'mode' => cache_store::MODE_APPLICATION,
+ 'simplekeys' => true,
+ 'simpledata' => false,
+ 'staticacceleration' => true,
+ 'staticaccelerationsize' => 50,
+ ),
'contextlevel' => array(
'mode' => cache_store::MODE_APPLICATION,
'simplekeys' => true,
<?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"
>
<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"/>
<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>
\ No newline at end of file
upgrade_plugin_savepoint(true, 2018082100, 'tool', 'dataprivacy');
}
+ if ($oldversion < 2018100401) {
+ // 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, 2018100401, 'tool', 'dataprivacy');
+ }
+
return true;
}
$returnurl = new \moodle_url('/admin/tool/dataprivacy/purposes.php');
if ($form->is_cancelled()) {
redirect($returnurl);
-} else if ($data = $form->get_data()) {
+} else if ($alldata = $form->get_data()) {
+ $data = $form->filter_data_for_persistent($alldata);
+
if (empty($data->id)) {
- \tool_dataprivacy\api::create_purpose($data);
+ $purpose = \tool_dataprivacy\api::create_purpose($data);
$messagesuccess = get_string('purposecreated', 'tool_dataprivacy');
} else {
- \tool_dataprivacy\api::update_purpose($data);
+ $purpose = \tool_dataprivacy\api::update_purpose($data);
$messagesuccess = get_string('purposeupdated', 'tool_dataprivacy');
}
+
+ $currentoverrides = [];
+ foreach ($purpose->get_purpose_overrides() as $override) {
+ $currentoverrides[$override->get('id')] = $override;
+ }
+
+ $overrides = $form->get_role_overrides_from_data($alldata);
+ $submittedoverrides = [];
+ $tosave = [];
+
+ foreach ($overrides as $overridedata) {
+ $overridedata->purposeid = $purpose->get('id');
+ $override = new \tool_dataprivacy\purpose_override($overridedata->id, $overridedata);
+
+ $tosave[] = $override;
+
+ if (!empty($overridedata->id)) {
+ $submittedoverrides[$overridedata->id] = true;
+ }
+ }
+
+ foreach ($currentoverrides as $id => $override) {
+ if (!isset($submittedoverrides[$id])) {
+ $override->delete();
+ }
+ }
+
+ foreach ($tosave as $override) {
+ $override->save();
+ }
+
redirect($returnurl, $messagesuccess, 0, \core\output\notification::NOTIFY_SUCCESS);
}
$string['bulkapproverequests'] = 'Approve requests';
$string['bulkdenyrequests'] = 'Deny requests';
$string['cachedef_purpose'] = 'Data purposes';
+$string['cachedef_purpose_overrides'] = 'Purpose overrides in the Data Privacy tool';
$string['cachedef_contextlevel'] = 'Context levels purpose and category';
$string['cancelrequest'] = 'Cancel request';
$string['cancelrequestconfirmation'] = 'Do you really want cancel this data request?';
$string['nosubjectaccessrequests'] = 'There are no data requests that you need to act on';
$string['nosystemdefaults'] = 'Site purpose and category have not yet been defined.';
$string['notset'] = 'Not set (use the default value)';
+$string['notyetexpired'] = '{$a} (not yet expired)';
$string['overrideinstances'] = 'Reset instances with custom values';
$string['pluginregistry'] = 'Plugin privacy registry';
$string['pluginregistrytitle'] = 'Plugin privacy compliance registry';
$string['retentionperiod_help'] = 'The retention period specifies the length of time that data should be kept for. When the retention period has expired, the data is flagged and listed for deletion, awaiting admin confirmation.';
$string['retentionperiodnotdefined'] = 'No retention period was defined';
$string['retentionperiodzero'] = 'No retention period';
+$string['roleoverrides'] = 'Role overrides';
$string['selectbulkaction'] = 'Please select a bulk action.';
$string['selectdatarequests'] = 'Please select data requests.';
$string['selectuserdatarequest'] = 'Select {$a->username}\'s {$a->requesttype} data request.';
$string['user'] = 'User';
$string['viewrequest'] = 'View the request';
$string['visible'] = 'Expand all';
+$string['unexpiredrolewithretention'] = '{$a->retention} (Unexpired)';
+$string['expiredrolewithretention'] = '{$a->retention} (Expired)';
+$string['defaultexpired'] = 'Data for all users';
+$string['defaultexpiredexcept'] = 'Data for all users, except those who hold any of the following roles:<br>
+{$a->unexpired}';
+$string['defaultunexpiredwithexceptions'] = 'Only data for users who hold any of the following roles:<br>
+{$a->expired}
+Unless they also hold any of the following roles:<br>
+{$a->unexpired}';
+$string['defaultunexpired'] = 'Only data for users holding any of the following roles:<br>
+{$a->expired}';
+$string['tobedeleted'] = 'Data to be deleted';
+$string['addroleoverride'] = 'Add role override';
+$string['roleoverride'] = 'Role override';
+$string['role'] = 'Role';
+$string['role_help'] = 'Which role do you wish to apply this override to';
+$string['duplicaterole'] = 'Role already specified';
+$string['purposeoverview'] = 'A purpose describes the intended use and retention policy for stored data. The basis for storing and retaining that data is also described in the purpose.';
+$string['roleoverrideoverview'] = 'The default retention policy can be overridden for specific user roles, allowing you to specify a longer, or a shorter, retention policy. A user is only expired when all of their roles have expired.';
}}
{{#navigation}}
- {{> core/action_link}}
+ <div class="m-b-1">
+ {{> core/action_link}}
+ </div>
{{/navigation}}
-<div data-region="purposes" class="m-t-3 m-b-1">
+<p>
+ {{#str}}purposeoverview, tool_dataprivacy{{/str}}
+</p>
+
+<div data-region="purposes" class="m-b-1">
<div class="m-y-1">
<button class="btn btn-secondary" data-add-element="purpose" title="{{#str}}addpurpose, tool_dataprivacy{{/str}}">
{{#pix}}t/add, moodle, {{#str}}addpurpose, tool_dataprivacy{{/str}}{{/pix}}
<th scope="col">{{#str}}sensitivedatareasons, tool_dataprivacy{{/str}}</th>
<th scope="col">{{#str}}retentionperiod, tool_dataprivacy{{/str}}</th>
<th scope="col">{{#str}}protected, tool_dataprivacy{{/str}}</th>
+ <th scope="col">{{#str}}roleoverrides, tool_dataprivacy{{/str}}</th>
<th scope="col">{{#str}}actions{{/str}}</th>
</tr>
</thead>
{{#str}}no{{/str}}
{{/protected}}
</td>
+ <td>
+ {{#roleoverrides}}
+ {{#str}}yes{{/str}}
+ {{/roleoverrides}}
+ {{^roleoverrides}}
+ {{#str}}no{{/str}}
+ {{/roleoverrides}}
+ </td>
<td>
{{#actions}}
{{> core/action_menu}}
use core\invalid_persistent_exception;
use core\task\manager;
+use tool_dataprivacy\contextlist_context;
use tool_dataprivacy\context_instance;
use tool_dataprivacy\api;
use tool_dataprivacy\data_registry;
use tool_dataprivacy\expired_context;
use tool_dataprivacy\data_request;
+use tool_dataprivacy\purpose;
+use tool_dataprivacy\category;
use tool_dataprivacy\local\helper;
use tool_dataprivacy\task\initiate_data_request_task;
use tool_dataprivacy\task\process_data_request_task;
$this->assertEquals($data->contexts->used, $contextids, '', 0.0, 10, true);
}
+ /**
+ * Test that delete requests do not filter out protected purpose contexts if they are already expired.
+ */
+ public function test_add_request_contexts_with_status_delete_course_expired_protected() {
+ global $DB;
+
+ $this->resetAfterTest();
+
+ $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
+ $purposes->course->set('protected', 1)->save();
+
+ $user = $this->getDataGenerator()->create_user();
+ $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - YEARSECS]);
+ $coursecontext = \context_course::instance($course->id);
+
+ $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
+
+ $collection = new \core_privacy\local\request\contextlist_collection($user->id);
+ $contextlist = new \core_privacy\local\request\contextlist();
+ $contextlist->set_component('tool_dataprivacy');
+ $contextlist->add_from_sql('SELECT id FROM {context} WHERE id IN(:ctx1)', ['ctx1' => $coursecontext->id]);
+ $collection->add_contextlist($contextlist);
+
+ $request = api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE);
+
+ $purposes->course->set('protected', 1)->save();
+ api::add_request_contexts_with_status($collection, $request->get('id'), contextlist_context::STATUS_APPROVED);
+
+ $requests = contextlist_context::get_records();
+ $this->assertCount(1, $requests);
+ }
+
+ /**
+ * Test that delete requests does filter out protected purpose contexts which are not expired.
+ */
+ public function test_add_request_contexts_with_status_delete_course_unexpired_protected() {
+ global $DB;
+
+ $this->resetAfterTest();
+
+ $purposes = $this->setup_basics('PT1H', 'PT1H', 'P1Y');
+ $purposes->course->set('protected', 1)->save();
+
+ $user = $this->getDataGenerator()->create_user();
+ $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time()]);
+ $coursecontext = \context_course::instance($course->id);
+
+ $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
+
+ $collection = new \core_privacy\local\request\contextlist_collection($user->id);
+ $contextlist = new \core_privacy\local\request\contextlist();
+ $contextlist->set_component('tool_dataprivacy');
+ $contextlist->add_from_sql('SELECT id FROM {context} WHERE id IN(:ctx1)', ['ctx1' => $coursecontext->id]);
+ $collection->add_contextlist($contextlist);
+
+ $request = api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE);
+
+ $purposes->course->set('protected', 1)->save();
+ api::add_request_contexts_with_status($collection, $request->get('id'), contextlist_context::STATUS_APPROVED);
+
+ $requests = contextlist_context::get_records();
+ $this->assertCount(0, $requests);
+ }
+
+ /**
+ * Test that delete requests do not filter out unexpired contexts if they are not protected.
+ */
+ public function test_add_request_contexts_with_status_delete_course_unexpired_unprotected() {
+ global $DB;
+
+ $this->resetAfterTest();
+
+ $purposes = $this->setup_basics('PT1H', 'PT1H', 'P1Y');
+ $purposes->course->set('protected', 1)->save();
+
+ $user = $this->getDataGenerator()->create_user();
+ $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time()]);
+ $coursecontext = \context_course::instance($course->id);
+
+ $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
+
+ $collection = new \core_privacy\local\request\contextlist_collection($user->id);
+ $contextlist = new \core_privacy\local\request\contextlist();
+ $contextlist->set_component('tool_dataprivacy');
+ $contextlist->add_from_sql('SELECT id FROM {context} WHERE id IN(:ctx1)', ['ctx1' => $coursecontext->id]);
+ $collection->add_contextlist($contextlist);
+
+ $request = api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE);
+
+ $purposes->course->set('protected', 0)->save();
+ api::add_request_contexts_with_status($collection, $request->get('id'), contextlist_context::STATUS_APPROVED);
+
+ $requests = contextlist_context::get_records();
+ $this->assertCount(1, $requests);
+ }
+
+ /**
+ * Test that delete requests do not filter out protected purpose contexts if they are already expired.
+ */
+ public function test_get_approved_contextlist_collection_for_request_delete_course_expired_protected() {
+ $this->resetAfterTest();
+
+ $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
+ $purposes->course->set('protected', 1)->save();
+
+ $user = $this->getDataGenerator()->create_user();
+ $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - YEARSECS]);
+ $coursecontext = \context_course::instance($course->id);
+
+ $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
+
+ // Create the request, with its contextlist and context.
+ $request = api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE);
+ $contextlist = new \tool_dataprivacy\contextlist(0, (object) ['component' => 'tool_dataprivacy']);
+ $contextlist->save();
+
+ $clcontext = new \tool_dataprivacy\contextlist_context(0, (object) [
+ 'contextid' => $coursecontext->id,
+ 'status' => contextlist_context::STATUS_APPROVED,
+ 'contextlistid' => $contextlist->get('id'),
+ ]);
+ $clcontext->save();
+
+ $rcl = new \tool_dataprivacy\request_contextlist(0, (object) [
+ 'requestid' => $request->get('id'),
+ 'contextlistid' => $contextlist->get('id'),
+ ]);
+ $rcl->save();
+
+ $purposes->course->set('protected', 1)->save();
+ $collection = api::get_approved_contextlist_collection_for_request($request);
+
+ $this->assertCount(1, $collection);
+
+ $list = $collection->get_contextlist_for_component('tool_dataprivacy');
+ $this->assertCount(1, $list);
+ }
+
+ /**
+ * Test that delete requests does filter out protected purpose contexts which are not expired.
+ */
+ public function test_get_approved_contextlist_collection_for_request_delete_course_unexpired_protected() {
+ $this->resetAfterTest();
+
+ $purposes = $this->setup_basics('PT1H', 'PT1H', 'P1Y');
+ $purposes->course->set('protected', 1)->save();
+
+ $user = $this->getDataGenerator()->create_user();
+ $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time()]);
+ $coursecontext = \context_course::instance($course->id);
+
+ $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
+
+ // Create the request, with its contextlist and context.
+ $request = api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE);
+ $contextlist = new \tool_dataprivacy\contextlist(0, (object) ['component' => 'tool_dataprivacy']);
+ $contextlist->save();
+
+ $clcontext = new \tool_dataprivacy\contextlist_context(0, (object) [
+ 'contextid' => $coursecontext->id,
+ 'status' => contextlist_context::STATUS_APPROVED,
+ 'contextlistid' => $contextlist->get('id'),
+ ]);
+ $clcontext->save();
+
+ $rcl = new \tool_dataprivacy\request_contextlist(0, (object) [
+ 'requestid' => $request->get('id'),
+ 'contextlistid' => $contextlist->get('id'),
+ ]);
+ $rcl->save();
+
+ $purposes->course->set('protected', 1)->save();
+ $collection = api::get_approved_contextlist_collection_for_request($request);
+
+ $this->assertCount(0, $collection);
+
+ $list = $collection->get_contextlist_for_component('tool_dataprivacy');
+ $this->assertEmpty($list);
+ }
+
+ /**
+ * Test that delete requests do not filter out unexpired contexts if they are not protected.
+ */
+ public function test_get_approved_contextlist_collection_for_request_delete_course_unexpired_unprotected() {
+ $this->resetAfterTest();
+
+ $purposes = $this->setup_basics('PT1H', 'PT1H', 'P1Y');
+ $purposes->course->set('protected', 1)->save();
+
+ $user = $this->getDataGenerator()->create_user();
+ $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time()]);
+ $coursecontext = \context_course::instance($course->id);
+
+ $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
+
+ // Create the request, with its contextlist and context.
+ $request = api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE);
+ $contextlist = new \tool_dataprivacy\contextlist(0, (object) ['component' => 'tool_dataprivacy']);
+ $contextlist->save();
+
+ $clcontext = new \tool_dataprivacy\contextlist_context(0, (object) [
+ 'contextid' => $coursecontext->id,
+ 'status' => contextlist_context::STATUS_APPROVED,
+ 'contextlistid' => $contextlist->get('id'),
+ ]);
+ $clcontext->save();
+
+ $rcl = new \tool_dataprivacy\request_contextlist(0, (object) [
+ 'requestid' => $request->get('id'),
+ 'contextlistid' => $contextlist->get('id'),
+ ]);
+ $rcl->save();
+
+ $purposes->course->set('protected', 0)->save();
+ $collection = api::get_approved_contextlist_collection_for_request($request);
+
+ $this->assertCount(1, $collection);
+
+ $list = $collection->get_contextlist_for_component('tool_dataprivacy');
+ $this->assertCount(1, $list);
+ }
+
/**
* Data provider for \tool_dataprivacy_api_testcase::test_set_context_defaults
*/
'list' => $approvedcollection->get_contextlist_for_component('tool_dataprivacy'),
];
}
+
+ /**
+ * Setup the basics with the specified retention period.
+ *
+ * @param string $system Retention policy for the system.
+ * @param string $user Retention policy for users.
+ * @param string $course Retention policy for courses.
+ * @param string $activity Retention policy for activities.
+ */
+ protected function setup_basics(string $system, string $user, string $course = null, string $activity = null) : \stdClass {
+ $this->resetAfterTest();
+
+ $purposes = (object) [
+ 'system' => $this->create_and_set_purpose_for_contextlevel($system, CONTEXT_SYSTEM),
+ 'user' => $this->create_and_set_purpose_for_contextlevel($user, CONTEXT_USER),
+ ];
+
+ if (null !== $course) {
+ $purposes->course = $this->create_and_set_purpose_for_contextlevel($course, CONTEXT_COURSE);
+ }
+
+ if (null !== $activity) {
+ $purposes->activity = $this->create_and_set_purpose_for_contextlevel($activity, CONTEXT_MODULE);
+ }
+
+ return $purposes;
+ }
+
+ /**
+ * Create a retention period and set it for the specified context level.
+ *
+ * @param string $retention
+ * @param int $contextlevel
+ * @return purpose
+ */
+ protected function create_and_set_purpose_for_contextlevel(string $retention, int $contextlevel) : purpose {
+ $purpose = new purpose(0, (object) [
+ 'name' => 'Test purpose ' . rand(1, 1000),
+ 'retentionperiod' => $retention,
+ 'lawfulbases' => 'gdpr_art_6_1_a',
+ ]);
+ $purpose->create();
+
+ $cat = new category(0, (object) ['name' => 'Test category']);
+ $cat->create();
+
+ if ($contextlevel <= CONTEXT_USER) {
+ $record = (object) [
+ 'purposeid' => $purpose->get('id'),
+ 'categoryid' => $cat->get('id'),
+ 'contextlevel' => $contextlevel,
+ ];
+ api::set_contextlevel($record);
+ } else {
+ list($purposevar, ) = data_registry::var_names_from_context(
+ \context_helper::get_class_for_level(CONTEXT_COURSE)
+ );
+ set_config($purposevar, $purpose->get('id'), 'tool_dataprivacy');
+ }
+
+ return $purpose;
+ }
}
And I should see "Legal obligation (GDPR Art 6.1(c))" in the "Purpose 1" "table_row"
And I should see "Explicit consent (GDPR Art. 9.2(a))" in the "Purpose 1" "table_row"
And I should see "2 years" in the "Purpose 1" "table_row"
- And I should see "No" in the "Purpose 1" "table_row"
+ And "Purpose 1 Purpose 1 description" row "5" column of "List of data purposes" table should contain "No"
Scenario: Update a data storage purpose
Given I click on "Actions" "link" in the "Purpose 1" "table_row"
And I should see "Vital interests (GDPR Art. 6.1(d))" in the "Purpose 1 edited" "table_row"
And I should see "3 years" in the "Purpose 1 edited" "table_row"
But I should not see "Legal obligation (GDPR Art 6.1(c))" in the "Purpose 1 edited" "table_row"
- And I should not see "No" in the "Purpose 1 edited" "table_row"
+ And "Purpose 1 edited Purpose 1 description edited" row "5" column of "List of data purposes" table should not contain "No"
Scenario: Delete a data storage purpose
Given I click on "Actions" "link" in the "Purpose 1" "table_row"
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;
+use tool_dataprivacy\expired_contexts_manager;
defined('MOODLE_INTERNAL') || die();
global $CFG;
* @param string $course Retention policy for courses.
* @param string $activity Retention policy for activities.
*/
- protected function setup_basics(string $system, string $user, string $course, string $activity = null) : array {
+ protected function setup_basics(string $system, string $user, string $course = null, string $activity = null) : \stdClass {
$this->resetAfterTest();
- $purposes = [];
- $purposes[] = $this->create_and_set_purpose_for_contextlevel($system, CONTEXT_SYSTEM);
- $purposes[] = $this->create_and_set_purpose_for_contextlevel($user, CONTEXT_USER);
- $purposes[] = $this->create_and_set_purpose_for_contextlevel($course, CONTEXT_COURSE);
+ $purposes = (object) [
+ 'system' => $this->create_and_set_purpose_for_contextlevel($system, CONTEXT_SYSTEM),
+ 'user' => $this->create_and_set_purpose_for_contextlevel($user, CONTEXT_USER),
+ ];
+
+ if (null !== $course) {
+ $purposes->course = $this->create_and_set_purpose_for_contextlevel($course, CONTEXT_COURSE);
+ }
+
if (null !== $activity) {
- $purposes[] = $this->create_and_set_purpose_for_contextlevel($activity, CONTEXT_MODULE);
+ $purposes->activity = $this->create_and_set_purpose_for_contextlevel($activity, CONTEXT_MODULE);
}
return $purposes;
$this->assertEquals(0, $flaggedusers);
}
+ /**
+ * Ensure that a user with a lastaccess in the past and no active enrolments is flagged for deletion.
+ */
+ public function test_flag_user_update_existing() {
+ $this->resetAfterTest();
+
+ $this->setup_basics('PT1H', 'PT1H', 'P5Y');
+
+ $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
+ $usercontext = \context_user::instance($user->id);
+
+ // Create an existing expired_context.
+ $expiredcontext = new expired_context(0, (object) [
+ 'contextid' => $usercontext->id,
+ 'defaultexpired' => 0,
+ 'status' => expired_context::STATUS_EXPIRED,
+ ]);
+ $expiredcontext->save();
+ $this->assertEquals(0, $expiredcontext->get('defaultexpired'));
+
+ // Flag all expired contexts.
+ $manager = new \tool_dataprivacy\expired_contexts_manager();
+ list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
+
+ $this->assertEquals(0, $flaggedcourses);
+ $this->assertEquals(1, $flaggedusers);
+
+ // The user context will now have expired.
+ $updatedcontext = new expired_context($expiredcontext->get('id'));
+ $this->assertEquals(1, $updatedcontext->get('defaultexpired'));
+ }
+
/**
* Ensure that a user with a lastaccess in the past and expired enrolments.
*/
$this->assertEquals(0, $flaggedusers);
}
+ /**
+ * Ensure that a user with a lastaccess in the past and expired enrolments.
+ */
+ public function test_flag_user_past_override_role() {
+ global $DB;
+ $this->resetAfterTest();
+
+ $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
+
+ $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' => $purposes->user->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.
*/
public function test_flag_user_retention_changed() {
$this->resetAfterTest();
- list($systempurpose, $userpurpose, $coursepurpose) = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
+ $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
$user = $this->getDataGenerator()->create_user(['lastaccess' => time() - DAYSECS]);
$usercontext = \context_user::instance($user->id);
$this->assertNotFalse($expiredcontext);
// Increase the retention period to 5 years.
- $userpurpose->set('retentionperiod', 'P5Y');
- $userpurpose->save();
+ $purposes->user->set('retentionperiod', 'P5Y');
+ $purposes->user->save();
// Re-run the expiry job - the previously flagged user will be removed because the retention period has been increased.
list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
public function test_flag_user_historic_block_unapproved() {
$this->resetAfterTest();
- list($systempurpose, $userpurpose, $coursepurpose) = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
+ $this->setup_basics('PT1H', 'PT1H', 'PT1H');
$user = $this->getDataGenerator()->create_user(['lastaccess' => time() - DAYSECS]);
$usercontext = \context_user::instance($user->id);
public function test_flag_user_historic_unexpired_child() {
$this->resetAfterTest();
- list($systempurpose, $userpurpose, $coursepurpose) = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
- $blockpurpose = $this->create_and_set_purpose_for_contextlevel('P5Y', CONTEXT_BLOCK);
+ $this->setup_basics('PT1H', 'PT1H', 'PT1H');
+ $this->create_and_set_purpose_for_contextlevel('P5Y', CONTEXT_BLOCK);
$user = $this->getDataGenerator()->create_user(['lastaccess' => time() - DAYSECS]);
$usercontext = \context_user::instance($user->id);
]);
$forum2 = $this->getDataGenerator()->create_module('forum', ['course' => $course2->id]);
- // Flag all expired contexts.
- $manager = new \tool_dataprivacy\expired_contexts_manager();
- list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
+ // Flag all expired contexts.
+ $manager = new \tool_dataprivacy\expired_contexts_manager();
+ list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
+
+ $this->assertEquals(4, $flaggedcourses);
+ $this->assertEquals(0, $flaggedusers);
+ }
+
+ /**
+ * Ensure that a course with an end date in the future is not flagged.
+ */
+ public function test_flag_course_future_enddate() {
+ $this->resetAfterTest();
+
+ $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
+
+ $course = $this->getDataGenerator()->create_course(['enddate' => time() + YEARSECS]);
+ $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->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);
+ }
+
+ /**
+ * Ensure that a course with an end date in the future is not flagged.
+ */
+ public function test_flag_course_recent_unexpired_enddate() {
+ $this->resetAfterTest();
+
+ $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
+
+ $course = $this->getDataGenerator()->create_course(['enddate' => time() - 1]);
+
+ // 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);
+ }
+
+ /**
+ * 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');
+
+ $role = $DB->get_record('role', ['shortname' => 'editingteacher']);
+
+ $override = new purpose_override(0, (object) [
+ 'purposeid' => $purposes->course->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');
+
+ $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' => $purposes->course->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');
+
+ $role = $DB->get_record('role', ['shortname' => 'editingteacher']);
+
+ (new purpose_override(0, (object) [
+ 'purposeid' => $purposes->course->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');
+
+ $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' => $purposes->user->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');
+
+ $role = $DB->get_record('role', ['shortname' => 'editingteacher']);
+
+ $override = new purpose_override(0, (object) [
+ 'purposeid' => $purposes->course->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');
+
+ $role = $DB->get_record('role', ['shortname' => 'student']);
+
+ $override = new purpose_override(0, (object) [
+ 'purposeid' => $purposes->course->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');
+
+ $role = $DB->get_record('role', ['shortname' => 'student']);
+
+ $override = new purpose_override(0, (object) [
+ 'purposeid' => $purposes->course->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);
- $this->assertEquals(4, $flaggedcourses);
- $this->assertEquals(0, $flaggedusers);
+ $updatedcontext = new expired_context($expiredcontext->get('id'));
+ $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
}
/**
- * Ensure that a course with an end date in the future is not flagged.
+ * Ensure that a module context previously flagged as approved is removed with appropriate expiredroles kept.
*/
- public function test_flag_course_future_enddate() {
+ public function test_process_course_context_with_user_in_both_lists_expired() {
+ global $DB;
$this->resetAfterTest();
- $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
+ $purposes = $this->setup_basics('PT1H', 'PT1H', 'P5Y');
- $course = $this->getDataGenerator()->create_course(['enddate' => time() + YEARSECS]);
+ $studentrole = $DB->get_record('role', ['shortname' => 'student']);
+ $override = new purpose_override(0, (object) [
+ 'purposeid' => $purposes->course->get('id'),
+ 'roleid' => $studentrole->id,
+ 'retentionperiod' => 'PT1M',
+ ]);
+ $override->save();
+
+ $teacherrole = $DB->get_record('role', ['shortname' => 'editingteacher']);
+ $override = new purpose_override(0, (object) [
+ 'purposeid' => $purposes->course->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,
+ ]);
- // Flag all expired contexts.
- $manager = new \tool_dataprivacy\expired_contexts_manager();
- list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
+ $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,
+ ]);
- $this->assertEquals(0, $flaggedcourses);
- $this->assertEquals(0, $flaggedusers);
- }
+ // 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();
- /**
- * Ensure that a course with an end date in the future is not flagged.
- */
- public function test_flag_course_recent_unexpired_enddate() {
- $this->resetAfterTest();
+ $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;
+ }));
- $this->setup_basics('PT1H', 'PT1H', 'PT1H', 'PT1H');
+ $manager = $this->getMockBuilder(\tool_dataprivacy\expired_contexts_manager::class)
+ ->setMethods(['get_privacy_manager'])
+ ->getMock();
- $course = $this->getDataGenerator()->create_course(['enddate' => time() - 1]);
+ $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
+ $manager->set_progress(new \null_progress_trace());
+ list($processedcourses, $processedusers) = $manager->process_approved_deletions();
- // Flag all expired contexts.
- $manager = new \tool_dataprivacy\expired_contexts_manager();
- list($flaggedcourses, $flaggedusers) = $manager->flag_expired_contexts();
+ $this->assertEquals(1, $processedcourses);
+ $this->assertEquals(0, $processedusers);
- $this->assertEquals(0, $flaggedcourses);
- $this->assertEquals(0, $flaggedusers);
+ $updatedcontext = new expired_context($expiredcontext->get('id'));
+ $this->assertEquals(expired_context::STATUS_CLEANED, $updatedcontext->get('status'));
}
/**
public function test_process_user_historic_block_unapproved() {
$this->resetAfterTest();
- list($systempurpose, $userpurpose, $coursepurpose) = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
+ $this->setup_basics('PT1H', 'PT1H', 'PT1H');
$user = $this->getDataGenerator()->create_user(['lastaccess' => time() - DAYSECS]);
$usercontext = \context_user::instance($user->id);
public function test_process_user_historic_unexpired_child() {
$this->resetAfterTest();
- list($systempurpose, $userpurpose, $coursepurpose) = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
- $blockpurpose = $this->create_and_set_purpose_for_contextlevel('P5Y', CONTEXT_BLOCK);
+ $this->setup_basics('PT1H', 'PT1H', 'PT1H');
+ $this->create_and_set_purpose_for_contextlevel('P5Y', CONTEXT_BLOCK);
$user = $this->getDataGenerator()->create_user(['lastaccess' => time() - DAYSECS]);
$usercontext = \context_user::instance($user->id);
$manager->set_progress(new \null_progress_trace());
$manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
- $coursepurpose = $purposes[2];
- $coursepurpose->set('retentionperiod', 'P5Y');
- $coursepurpose->save();
+ $purposes->course->set('retentionperiod', 'P5Y');
+ $purposes->course->save();
list($processedcourses, $processedusers) = $manager->process_approved_deletions();
];
}
+ /**
+ * 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.
*/
return $block;
}
+
+ /**
+ * Test the is_context_expired functions when supplied with the system context.
+ */
+ public function test_is_context_expired_system() {
+ global $DB;
+
+ $this->resetAfterTest();
+ $this->setup_basics('PT1H', 'PT1H', 'P1D');
+ $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
+
+ $this->assertFalse(expired_contexts_manager::is_context_expired(\context_system::instance()));
+ $this->assertFalse(
+ expired_contexts_manager::is_context_expired_or_unprotected_for_user(\context_system::instance(), $user));
+ }
+
+ /**
+ * Test the is_context_expired functions when supplied with an expired course.
+ */
+ public function test_is_context_expired_course_expired() {
+ global $DB;
+
+ $this->resetAfterTest();
+
+ $purposes = $this->setup_basics('PT1H', 'PT1H', 'P1D');
+
+ $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
+ $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time()]);
+ $coursecontext = \context_course::instance($course->id);
+
+ $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
+
+ $this->assertFalse(expired_contexts_manager::is_context_expired($coursecontext));
+
+ $purposes->course->set('protected', 1)->save();
+ $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
+
+ $purposes->course->set('protected', 0)->save();
+ $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
+ }
+
+ /**
+ * Test the is_context_expired functions when supplied with an unexpired course.
+ */
+ public function test_is_context_expired_course_unexpired() {
+ global $DB;
+
+ $this->resetAfterTest();
+
+ $purposes = $this->setup_basics('PT1H', 'PT1H', 'P1D');
+
+ $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
+ $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]);
+ $coursecontext = \context_course::instance($course->id);
+
+ $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
+
+ $this->assertTrue(expired_contexts_manager::is_context_expired($coursecontext));
+
+ $purposes->course->set('protected', 1)->save();
+ $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
+
+ $purposes->course->set('protected', 0)->save();
+ $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
+ }
+
+ /**
+ * Test the is_context_expired functions when supplied with an expired course which has role overrides.
+ */
+ public function test_is_context_expired_course_expired_override() {
+ global $DB;
+
+ $this->resetAfterTest();
+
+ $purposes = $this->setup_basics('PT1H', 'PT1H', 'PT1H');
+
+ $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
+ $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]);
+ $coursecontext = \context_course::instance($course->id);
+ $systemcontext = \context_system::instance();
+
+ $role = $DB->get_record('role', ['shortname' => 'manager']);
+ $override = new purpose_override(0, (object) [
+ 'purposeid' => $purposes->course->get('id'),
+ 'roleid' => $role->id,
+ 'retentionperiod' => 'P5Y',
+ ]);
+ $override->save();
+ role_assign($role->id, $user->id, $systemcontext->id);
+
+ $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
+
+ $this->assertFalse(expired_contexts_manager::is_context_expired($coursecontext));
+
+ $purposes->course->set('protected', 1)->save();
+ $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
+
+ $purposes->course->set('protected', 0)->save();
+ $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
+ }
+
+ /**
+ * Test the is_context_expired functions when supplied with an expired course which has role overrides.
+ */
+ public function test_is_context_expired_course_expired_override_parent() {
+ global $DB;
+
+ $this->resetAfterTest();
+
+ $purposes = $this->setup_basics('PT1H', 'PT1H');
+
+ $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
+ $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]);
+ $coursecontext = \context_course::instance($course->id);
+ $systemcontext = \context_system::instance();
+
+ $role = $DB->get_record('role', ['shortname' => 'manager']);
+ $override = new purpose_override(0, (object) [
+ 'purposeid' => $purposes->system->get('id'),
+ 'roleid' => $role->id,
+ 'retentionperiod' => 'P5Y',
+ ]);
+ $override->save();
+ role_assign($role->id, $user->id, $systemcontext->id);
+
+ $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
+
+ $this->assertFalse(expired_contexts_manager::is_context_expired($coursecontext));
+
+ // The user override applies to this user. THIs means that the default expiry has no effect.
+ $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
+
+ $purposes->system->set('protected', 1)->save();
+ $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
+
+ $purposes->system->set('protected', 0)->save();
+ $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
+
+ $override->set('protected', 1)->save();
+ $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
+
+ $purposes->system->set('protected', 1)->save();
+ $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
+
+ $purposes->system->set('protected', 0)->save();
+ $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
+
+ }
+
+ /**
+ * Test the is_context_expired functions when supplied with an expired course which has role overrides but the user
+ * does not hold the role.
+ */
+ public function test_is_context_expired_course_expired_override_parent_no_role() {
+ global $DB;
+
+ $this->resetAfterTest();
+
+ $purposes = $this->setup_basics('PT1H', 'PT1H');
+
+ $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
+ $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]);
+ $coursecontext = \context_course::instance($course->id);
+ $systemcontext = \context_system::instance();
+
+ $role = $DB->get_record('role', ['shortname' => 'manager']);
+ $override = new purpose_override(0, (object) [
+ 'purposeid' => $purposes->system->get('id'),
+ 'roleid' => $role->id,
+ 'retentionperiod' => 'P5Y',
+ ]);
+ $override->save();
+
+ $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
+
+ // This context is not _fully _ expired.
+ $this->assertFalse(expired_contexts_manager::is_context_expired($coursecontext));
+ }
+
+ /**
+ * Test the is_context_expired functions when supplied with an unexpired course which has role overrides.
+ */
+ public function test_is_context_expired_course_expired_override_inverse() {
+ global $DB;
+
+ $this->resetAfterTest();
+
+ $purposes = $this->setup_basics('P1Y', 'P1Y');
+
+ $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
+ $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]);
+ $coursecontext = \context_course::instance($course->id);
+ $systemcontext = \context_system::instance();
+
+ $role = $DB->get_record('role', ['shortname' => 'student']);
+ $override = new purpose_override(0, (object) [
+ 'purposeid' => $purposes->system->get('id'),
+ 'roleid' => $role->id,
+ 'retentionperiod' => 'PT1S',
+ ]);
+ $override->save();
+
+ $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
+
+ // This context is not _fully _ expired.
+ $this->assertFalse(expired_contexts_manager::is_context_expired($coursecontext));
+ }
+
+ /**
+ * Test the is_context_expired functions when supplied with an unexpired course which has role overrides.
+ */
+ public function test_is_context_expired_course_expired_override_inverse_parent() {
+ global $DB;
+
+ $this->resetAfterTest();
+
+ $purposes = $this->setup_basics('P1Y', 'P1Y');
+
+ $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
+ $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]);
+ $coursecontext = \context_course::instance($course->id);
+ $systemcontext = \context_system::instance();
+
+ $role = $DB->get_record('role', ['shortname' => 'manager']);
+ $override = new purpose_override(0, (object) [
+ 'purposeid' => $purposes->system->get('id'),
+ 'roleid' => $role->id,
+ 'retentionperiod' => 'PT1S',
+ ]);
+ $override->save();
+
+ $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
+ role_assign($role->id, $user->id, $systemcontext->id);
+
+ $studentrole = $DB->get_record('role', ['shortname' => 'student']);
+ role_unassign($studentrole->id, $user->id, $coursecontext->id);
+
+ // This context is not _fully _ expired.
+ $this->assertFalse(expired_contexts_manager::is_context_expired($coursecontext));
+ }
+
+ /**
+ * Test the is_context_expired functions when supplied with an unexpired course which has role overrides.
+ */
+ public function test_is_context_expired_course_expired_override_inverse_parent_not_assigned() {
+ global $DB;
+
+ $this->resetAfterTest();
+
+ $purposes = $this->setup_basics('P1Y', 'P1Y');
+
+ $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
+ $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - WEEKSECS]);
+ $coursecontext = \context_course::instance($course->id);
+ $systemcontext = \context_system::instance();
+
+ $role = $DB->get_record('role', ['shortname' => 'manager']);
+ $override = new purpose_override(0, (object) [
+ 'purposeid' => $purposes->system->get('id'),
+ 'roleid' => $role->id,
+ 'retentionperiod' => 'PT1S',
+ ]);
+ $override->save();
+
+ // Enrol the user in the course without any role.
+ $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
+ $studentrole = $DB->get_record('role', ['shortname' => 'student']);
+ role_unassign($studentrole->id, $user->id, $coursecontext->id);
+
+ // This context is not _fully _ expired.
+ $this->assertFalse(expired_contexts_manager::is_context_expired($coursecontext));
+ }
+
+ /**
+ * Ensure that context expired checks for a specific user taken into account roles.
+ */
+ public function test_is_context_expired_or_unprotected_for_user_role_mixtures_protected() {
+ global $DB;
+
+ $this->resetAfterTest();
+
+ $purposes = $this->setup_basics('PT1S', 'PT1S', 'PT1S');
+
+ $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - DAYSECS]);
+ $coursecontext = \context_course::instance($course->id);
+ $systemcontext = \context_system::instance();
+
+ $roles = $DB->get_records_menu('role', [], 'id', 'shortname, id');
+ $override = new purpose_override(0, (object) [
+ 'purposeid' => $purposes->course->get('id'),
+ 'roleid' => $roles['manager'],
+ 'retentionperiod' => 'P1W',
+ 'protected' => 1,
+ ]);
+ $override->save();
+
+ $s = $this->getDataGenerator()->create_user();
+ $this->getDataGenerator()->enrol_user($s->id, $course->id, 'student');
+
+ $t = $this->getDataGenerator()->create_user();
+ $this->getDataGenerator()->enrol_user($t->id, $course->id, 'teacher');
+
+ $sm = $this->getDataGenerator()->create_user();
+ $this->getDataGenerator()->enrol_user($sm->id, $course->id, 'student');
+ role_assign($roles['manager'], $sm->id, $coursecontext->id);
+
+ $m = $this->getDataGenerator()->create_user();
+ role_assign($roles['manager'], $m->id, $coursecontext->id);
+
+ $tm = $this->getDataGenerator()->create_user();
+ $this->getDataGenerator()->enrol_user($t->id, $course->id, 'teacher');
+ role_assign($roles['manager'], $tm->id, $coursecontext->id);
+
+ // The context should only be expired for users who are not a manager.
+ $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $s));
+ $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $t));
+ $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $sm));
+ $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $tm));
+ $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $m));
+
+ $override->set('protected', 0)->save();
+ $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $s));
+ $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $t));
+ $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $sm));
+ $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $tm));
+ $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $m));
+ }
+
+ /**
+ * Ensure that context expired checks for a specific user taken into account roles when retention is inversed.
+ */
+ public function test_is_context_expired_or_unprotected_for_user_role_mixtures_protected_inverse() {
+ global $DB;
+
+ $this->resetAfterTest();
+
+ $purposes = $this->setup_basics('P5Y', 'P5Y', 'P5Y');
+
+ $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() - DAYSECS]);
+ $coursecontext = \context_course::instance($course->id);
+ $systemcontext = \context_system::instance();
+
+ $roles = $DB->get_records_menu('role', [], 'id', 'shortname, id');
+ $override = new purpose_override(0, (object) [
+ 'purposeid' => $purposes->course->get('id'),
+ 'roleid' => $roles['student'],
+ 'retentionperiod' => 'PT1S',
+ ]);
+ $override->save();
+
+ $s = $this->getDataGenerator()->create_user();
+ $this->getDataGenerator()->enrol_user($s->id, $course->id, 'student');
+
+ $t = $this->getDataGenerator()->create_user();
+ $this->getDataGenerator()->enrol_user($t->id, $course->id, 'teacher');
+
+ $sm = $this->getDataGenerator()->create_user();
+ $this->getDataGenerator()->enrol_user($sm->id, $course->id, 'student');
+ role_assign($roles['manager'], $sm->id, $coursecontext->id);
+
+ $m = $this->getDataGenerator()->create_user();
+ role_assign($roles['manager'], $m->id, $coursecontext->id);
+
+ $tm = $this->getDataGenerator()->create_user();
+ $this->getDataGenerator()->enrol_user($t->id, $course->id, 'teacher');
+ role_assign($roles['manager'], $tm->id, $coursecontext->id);
+
+ // The context should only be expired for users who are only a student.
+ $purposes->course->set('protected', 1)->save();
+ $override->set('protected', 1)->save();
+ $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $s));
+ $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $t));
+ $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $sm));
+ $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $tm));
+ $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $m));
+
+ $purposes->course->set('protected', 0)->save();
+ $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $s));
+ $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $t));
+ $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $sm));
+ $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $tm));
+ $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $m));
+ }
}
--- /dev/null
+<?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/>.
+
+/**
+ * Unit tests for the filtered_userlist.
+ *
+ * @package tool_dataprivacy
+ * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Unit tests for the filtered_userlist.
+ *
+ * @package tool_dataprivacy
+ * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_dataprivacy_filtered_userlist_testcase extends advanced_testcase {
+ /**
+ * Test the apply_expired_contexts_filters function with arange of options.
+ *
+ * @dataProvider apply_expired_contexts_filters_provider
+ * @param array $initial The set of userids in the initial filterlist.
+ * @param array $expired The set of userids considered as expired.
+ * @param array $unexpired The set of userids considered as unexpired.
+ * @param array $expected The expected values.
+ */
+ public function test_apply_expired_contexts_filters(array $initial, array $expired, array $unexpired, array $expected) {
+ $userlist = $this->getMockBuilder(\tool_dataprivacy\filtered_userlist::class)
+ ->disableOriginalConstructor()
+ ->setMethods(null)
+ ->getMock();
+
+ $rc = new \ReflectionClass(\tool_dataprivacy\filtered_userlist::class);
+ $rcm = $rc->getMethod('set_userids');
+ $rcm->setAccessible(true);
+ $rcm->invoke($userlist, $initial);
+
+
+ $userlist->apply_expired_context_filters($expired, $unexpired);
+ $filtered = $userlist->get_userids();
+
+ sort($expected);
+ sort($filtered);
+ $this->assertEquals($expected, $filtered);
+ }
+
+ /**
+ * Data provider for the apply_expired_contexts_filters function.
+ *
+ * @return array
+ */
+ public function apply_expired_contexts_filters_provider() : array {
+ return [
+ // Entire list should be preserved.
+ 'No overrides' => [
+ 'users' => [1, 2, 3, 4, 5],
+ 'expired' => [],
+ 'unexpired' => [],
+ [1, 2, 3, 4, 5],
+ ],
+ // The list should be filtered to only keep the expired users.
+ 'Expired only' => [
+ 'users' => [1, 2, 3, 4, 5],
+ 'expired' => [2, 3, 4],
+ 'unexpired' => [],
+ 'expected' => [2, 3, 4],
+ ],
+ // The list should be filtered to remove any unexpired users.
+ 'Unexpired only' => [
+ 'users' => [1, 2, 3, 4, 5],
+ 'expired' => [],
+ 'unexpired' => [1, 5],
+ 'expected' => [2, 3, 4],
+ ],
+ // The list should be filtered to only keep expired users who are not on the unexpired list.
+ 'Combination of expired and unexpired' => [
+ 'users' => [1, 2, 3, 4, 5],
+ 'expired' => [1, 2, 3],
+ 'unexpired' => [1, 5],
+ 'expected' => [2, 3],
+ ],
+ ];
+ }
+}
defined('MOODLE_INTERNAL') || die;
-$plugin->version = 2018092500;
+$plugin->version = 2018100403;
$plugin->requires = 2018050800; // Moodle 3.5dev (Build 2018031600) and upwards.
$plugin->component = 'tool_dataprivacy';
*/
namespace tool_mobile\privacy;
defined('MOODLE_INTERNAL') || die();
-use \core_privacy\local\request\writer;
-use \core_privacy\local\metadata\collection;
-use \core_privacy\local\request\contextlist;
-use \core_privacy\local\request\approved_contextlist;
-use \core_privacy\local\request\transform;
+use core_privacy\local\request\writer;
+use core_privacy\local\metadata\collection;
+use core_privacy\local\request\contextlist;
+use core_privacy\local\request\approved_contextlist;
+use core_privacy\local\request\approved_userlist;
+use core_privacy\local\request\transform;
+use core_privacy\local\request\userlist;
/**
* Privacy provider for tool_mobile.
*/
class provider implements
\core_privacy\local\metadata\provider,
+ \core_privacy\local\request\core_userlist_provider,
\core_privacy\local\request\user_preference_provider,
\core_privacy\local\request\plugin\provider {
/**
return $contextlist;
}
+
+ /**
+ * Get the list of users who have data within a context.
+ *
+ * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
+ */
+ public static function get_users_in_context(userlist $userlist) {
+ $context = $userlist->get_context();
+
+ if (!is_a($context, \context_user::class)) {
+ return;
+ }
+
+ // Add users based on userkey.
+ \core_userkey\privacy\provider::get_user_contexts_with_script($userlist, $context, 'tool_mobile');
+ }
+
/**
* Export all user data for the specified user, in the specified contexts.
*
// Delete all the userkeys.
\core_userkey\privacy\provider::delete_userkeys('tool_mobile', $userid);
}
-}
\ No newline at end of file
+
+ /**
+ * Delete multiple users within a single context.
+ *
+ * @param approved_userlist $userlist The approved context and user information to delete information for.
+ */
+ public static function delete_data_for_users(approved_userlist $userlist) {
+ global $DB;
+ $context = $userlist->get_context();
+ $userids = $userlist->get_userids();
+ $userid = reset($userids);
+
+ // Only deleting data for the user ID in that user's user context should be valid.
+ if ($context->contextlevel !== CONTEXT_USER || count($userids) != 1 || $userid != $context->instanceid) {
+ return;
+ }
+
+ // Delete all the userkeys.
+ \core_userkey\privacy\provider::delete_userkeys('tool_mobile', $userid);
+ }
+}
*/
defined('MOODLE_INTERNAL') || die();
-use \core_privacy\local\request\writer;
-use \core_privacy\local\request\transform;
-use \core_privacy\local\request\approved_contextlist;
-use \tool_mobile\privacy\provider;
+use core_privacy\local\request\writer;
+use core_privacy\local\request\transform;
+use core_privacy\local\request\approved_contextlist;
+use core_privacy\local\request\approved_userlist;
+use tool_mobile\privacy\provider;
/**
* Unit tests for the tool_mobile implementation of the privacy API.
$this->assertEquals(get_string('privacy:metadata:preference:tool_mobile_autologin_request_last', 'tool_mobile'),
$prefs->tool_mobile_autologin_request_last->description);
}
+
/**
* Test getting the context for the user ID related to this plugin.
*/
$contextlist = provider::get_contexts_for_userid($user->id);
$this->assertEquals($context->id, $contextlist->current()->id);
}
+
+ /**
+ * Test getting the users for a context related to this plugin.
+ */
+ public function test_get_users_in_context() {
+ $component = 'tool_mobile';
+
+ // Create users and Mobile user keys.
+ $user1 = $this->getDataGenerator()->create_user();
+ $user2 = $this->getDataGenerator()->create_user();
+ $context1 = \context_user::instance($user1->id);
+ $context2 = \context_user::instance($user2->id);
+ $key1 = get_user_key('tool_mobile', $user1->id);
+ $key2 = get_user_key('tool_mobile', $user2->id);
+
+ // Ensure only user1 is found in context1.
+ $userlist = new \core_privacy\local\request\userlist($context1, $component);
+ provider::get_users_in_context($userlist);
+ $userids = $userlist->get_userids();
+ $userid = reset($userids);
+
+ $this->assertCount(1, $userids);
+ $this->assertEquals($user1->id, $userid);
+ }
+
/**
* Test that data is exported correctly for this plugin.
*/
$this->assertCount(1, $userkeydata->keys);
$this->assertEquals($key->script, reset($userkeydata->keys)->script);
}
+
/**
* Test for provider::delete_data_for_all_users_in_context().
*/
$count = $DB->count_records('user_private_key', ['script' => 'tool_mobile']);
$this->assertEquals(0, $count);
}
+
/**
* Test for provider::delete_data_for_user().
*/
$count = $DB->count_records('user_private_key', ['script' => 'tool_mobile']);
$this->assertEquals(0, $count);
}
-}
\ No newline at end of file
+
+ /**
+ * Test for provider::test_delete_data_for_users().
+ */
+ public function test_delete_data_for_users() {
+ global $DB;
+ $component = 'tool_mobile';
+
+ // Create users and Mobile user keys.
+ $user1 = $this->getDataGenerator()->create_user();
+ $user2 = $this->getDataGenerator()->create_user();
+ $context1 = \context_user::instance($user1->id);
+ $context2 = \context_user::instance($user2->id);
+ $keyvalue1 = get_user_key('tool_mobile', $user1->id);
+ $keyvalue2 = get_user_key('tool_mobile', $user2->id);
+ $key1 = $DB->get_record('user_private_key', ['value' => $keyvalue1]);
+
+ // Before deletion, we should have 2 user_private_keys.
+ $count = $DB->count_records('user_private_key', ['script' => 'tool_mobile']);
+ $this->assertEquals(2, $count);
+
+ // Ensure deleting wrong user in the user context does nothing.
+ $approveduserids = [$user2->id];
+ $approvedlist = new approved_userlist($context1, $component, $approveduserids);
+ provider::delete_data_for_users($approvedlist);
+
+ $count = $DB->count_records('user_private_key', ['script' => 'tool_mobile']);
+ $this->assertEquals(2, $count);
+
+ // Delete for user1 in context1.
+ $approveduserids = [$user1->id];
+ $approvedlist = new approved_userlist($context1, $component, $approveduserids);
+ provider::delete_data_for_users($approvedlist);
+
+ // Ensure only user1's data is deleted, user2's remains.
+ $count = $DB->count_records('user_private_key', ['script' => 'tool_mobile']);
+ $this->assertEquals(1, $count);
+
+ $params = ['script' => $component];
+ $userid = $DB->get_field_select('user_private_key', 'userid', 'script = :script', $params);
+ $this->assertEquals($user2->id, $userid);
+ }
+}
use \core_privacy\local\metadata\collection;
use \core_privacy\local\request\contextlist;
use \core_privacy\local\request\approved_contextlist;
+use \core_privacy\local\request\approved_userlist;
use \core_privacy\local\request\transform;
+use \core_privacy\local\request\userlist;
use \core_privacy\local\request\writer;
use \tool_monitor\subscription_manager;
use \tool_monitor\rule_manager;
* @copyright 2018 Adrian Greeve <adrian@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\provider {
+class provider implements
+ \core_privacy\local\metadata\provider,
+ \core_privacy\local\request\core_userlist_provider,
+ \core_privacy\local\request\plugin\provider {
/**
* Get information about the user data stored by this plugin.
return $contextlist;
}
+ /**
+ * Get the list of users who have data within a context.
+ *
+ * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
+ */
+ public static function get_users_in_context(userlist $userlist) {
+ $context = $userlist->get_context();
+
+ if (!is_a($context, \context_user::class)) {
+ return;
+ }
+
+ $params = [
+ 'contextid' => $context->id,
+ 'contextuser' => CONTEXT_USER,
+ ];
+
+ $sql = "SELECT mr.userid
+ FROM {context} ctx
+ JOIN {tool_monitor_rules} mr ON ctx.instanceid = mr.userid
+ AND ctx.contextlevel = :contextuser
+ WHERE ctx.id = :contextid";
+
+ $userlist->add_from_sql('userid', $sql, $params);
+
+ $sql = "SELECT ms.userid
+ FROM {context} ctx
+ LEFT JOIN {tool_monitor_subscriptions} ms ON ctx.instanceid = ms.userid
+ AND ctx.contextlevel = :contextuser
+ WHERE ctx.id = :contextid";
+
+ $userlist->add_from_sql('userid', $sql, $params);
+ }
+
/**
* Export all event monitor information for the list of contexts and this user.
*
static::delete_user_data($contextlist->get_user()->id);
}
+ /**
+ * Delete multiple users within a single context.
+ *
+ * @param approved_userlist $userlist The approved context and user information to delete information for.
+ */
+ public static function delete_data_for_users(approved_userlist $userlist) {
+ $context = $userlist->get_context();
+ $userids = $userlist->get_userids();
+ $userid = reset($userids);
+
+ // Only delete data for user context, which should be a single user.
+ if ($context->contextlevel == CONTEXT_USER && count($userids) == 1 && $userid == $context->instanceid) {
+ static::delete_user_data($userid);
+ }
+ }
+
/**
* This does the deletion of user data for the event monitor.
*
use \tool_monitor\privacy\provider;
use \core_privacy\local\request\approved_contextlist;
+use \core_privacy\local\request\approved_userlist;
/**
* Privacy test for the event monitor
$this->assertEquals($usercontext2->id, $contextlist->get_contextids()[0]);
}
+ /**
+ * Check that the correct userlist is returned if there is any user data for this context.
+ */
+ public function test_get_users_in_context() {
+ $component = 'tool_monitor';
+ $user = $this->getDataGenerator()->create_user();
+ $user2 = $this->getDataGenerator()->create_user();
+ $usercontext = \context_user::instance($user->id);
+ $usercontext2 = \context_user::instance($user2->id);
+
+ $userlist = new \core_privacy\local\request\userlist($usercontext, $component);
+ provider::get_users_in_context($userlist);
+ $this->assertEmpty($userlist);
+
+ $userlist = new \core_privacy\local\request\userlist($usercontext2, $component);
+ provider::get_users_in_context($userlist);
+ $this->assertEmpty($userlist);
+
+ $monitorgenerator = $this->getDataGenerator()->get_plugin_generator('tool_monitor');
+
+ // Create a rule with user.
+ $this->setUser($user);
+ $rule = $monitorgenerator->create_rule();
+ $userlist = new \core_privacy\local\request\userlist($usercontext, $component);
+ provider::get_users_in_context($userlist);
+
+ // Check that we only get back user.
+ $userids = $userlist->get_userids();
+ $this->assertCount(1, $userlist);
+ $this->assertEquals($user->id, $userids[0]);
+
+ // Create a subscription with user2.
+ $this->setUser($user2);
+
+ $record = new stdClass();
+ $record->courseid = 0;
+ $record->userid = $user2->id;
+ $record->ruleid = $rule->id;
+
+ $subscription = $monitorgenerator->create_subscription($record);
+ $userlist = new \core_privacy\local\request\userlist($usercontext2, $component);
+ provider::get_users_in_context($userlist);
+
+ // Check that user2 is returned for just subscribing to a rule.
+ $userids = $userlist->get_userids();
+ $this->assertCount(1, $userlist);
+ $this->assertEquals($user2->id, $userids[0]);
+ }
+
/**
* Test that user data is exported correctly.
*/
$this->assertEquals($user2->id, $dbsubs[$subscription2->id]->userid);
$this->assertEquals($user2->id, $dbsubs[$subscription3->id]->userid);
}
+
+ /**
+ * Test deleting user data for an approved userlist in a context.
+ */
+ public function test_delete_data_for_users() {
+ global $DB;
+
+ $component = 'tool_monitor';
+ $user = $this->getDataGenerator()->create_user();
+ $user2 = $this->getDataGenerator()->create_user();
+ $usercontext = \context_user::instance($user->id);
+ $usercontext2 = \context_user::instance($user2->id);
+ $monitorgenerator = $this->getDataGenerator()->get_plugin_generator('tool_monitor');
+
+ $this->setUser($user);
+ // Need to give user one the ability to manage rules.
+ $this->assign_user_capability('tool/monitor:managerules', \context_system::instance());
+
+ $rulerecord = (object)['name' => 'privacy rule'];
+ $rule = $monitorgenerator->create_rule($rulerecord);
+
+ $secondrulerecord = (object)['name' => 'privacy rule2'];
+ $rule2 = $monitorgenerator->create_rule($secondrulerecord);
+
+ $subscription = (object)['ruleid' => $rule->id, 'userid' => $user->id];
+ $subscription = $monitorgenerator->create_subscription($subscription);
+
+ // Have user 2 subscribe to the second rule created by user 1.
+ $subscription2 = (object)['ruleid' => $rule2->id, 'userid' => $user2->id];
+ $subscription2 = $monitorgenerator->create_subscription($subscription2);
+
+ $this->setUser($user2);
+ $thirdrulerecord = (object)['name' => 'privacy rule for second user'];
+ $rule3 = $monitorgenerator->create_rule($thirdrulerecord);
+
+ $subscription3 = (object)['ruleid' => $rule3->id, 'userid' => $user2->id];
+ $subscription3 = $monitorgenerator->create_subscription($subscription3);
+
+ // Get all of the monitor rules, ensure all exist.
+ $dbrules = $DB->get_records('tool_monitor_rules');
+ $this->assertCount(3, $dbrules);
+
+ // Delete for user2 in first user's context, should have no effect.
+ $approveduserids = [$user2->id];
+ $approvedlist = new approved_userlist($usercontext, $component, $approveduserids);
+ provider::delete_data_for_users($approvedlist);
+
+ $dbrules = $DB->get_records('tool_monitor_rules');
+ $this->assertCount(3, $dbrules);
+
+ // Delete for user in usercontext.
+ $approveduserids = [$user->id];
+ $approvedlist = new approved_userlist($usercontext, $component, $approveduserids);
+ provider::delete_data_for_users($approvedlist);
+
+ // Only the rules for user 1 that does not have any more subscriptions should be deleted (the first rule).
+ $dbrules = $DB->get_records('tool_monitor_rules');
+ $this->assertCount(2, $dbrules);
+ $this->assertEquals($user->id, $dbrules[$rule2->id]->userid);
+ $this->assertEquals($user2->id, $dbrules[$rule3->id]->userid);
+
+ // There should be two subscriptions left, both for user 2.
+ $dbsubs = $DB->get_records('tool_monitor_subscriptions');
+ $this->assertCount(2, $dbsubs);
+ $this->assertEquals($user2->id, $dbsubs[$subscription2->id]->userid);
+ $this->assertEquals($user2->id, $dbsubs[$subscription3->id]->userid);
+
+ // Delete for user2 in context 2.
+ $approveduserids = [$user2->id];
+ $approvedlist = new approved_userlist($usercontext2, $component, $approveduserids);
+ provider::delete_data_for_users($approvedlist);
+
+ // There should be no subscriptions left.
+ $dbsubs = $DB->get_records('tool_monitor_subscriptions');
+ $this->assertEmpty($dbsubs);
+ }
}
$PAGE->set_context($context);
$PAGE->set_url(new moodle_url('/admin/tool/policy/accept.php'));
+if (!in_array($action, ['accept', 'decline', 'revoke'])) {
+ throw new moodle_exception('invalidaccessparameter');
+}
+
if ($returnurl) {
$returnurl = new moodle_url($returnurl);
} else if (count($userids) == 1) {
redirect($returnurl);
}
-if ($action == 'revoke') {
- $title = get_string('revokedetails', 'tool_policy');
-} else {
- $title = get_string('consentdetails', 'tool_policy');
-}
-
$output = $PAGE->get_renderer('tool_policy');
echo $output->header();
-echo $output->heading($title);
+echo $output->heading(get_string('statusformtitle'.$action, 'tool_policy'));
$form->display();
echo $output->footer();
*/
AcceptOnBehalf.prototype.contextid = -1;
- /**
- * @var {Array} strings
- * @private
- */
- AcceptOnBehalf.prototype.stringKeys = [
- {
- key: 'consentdetails',
- component: 'tool_policy'
- },
- {
- key: 'iagreetothepolicy',
- component: 'tool_policy'
- },
- {
- key: 'selectusersforconsent',
- component: 'tool_policy'
- },
- {
- key: 'ok'
- },
- {
- key: 'revokedetails',
- component: 'tool_policy'
- },
- {
- key: 'irevokethepolicy',
- component: 'tool_policy'
- }
- ];
-
/**
* @var {object} currentTrigger The triggered HTML jQuery object
* @private
var formData = form.serialize();
this.showFormModal(formData);
} else {
- Str.get_strings(this.stringKeys).done(function(strings) {
- Notification.alert('', strings[2], strings[3]);
- });
+ Str.get_strings([
+ {key: 'notice'},
+ {key: 'selectusersforconsent', component: 'tool_policy'},
+ {key: 'ok'}
+ ]).then(function(strings) {
+ Notification.alert(strings[0], strings[1], strings[2]);
+ return;
+ }).fail(Notification.exception);
}
}.bind(this));
};
}
}
// Fetch the title string.
- Str.get_strings(this.stringKeys).done(function(strings) {
+ Str.get_strings([
+ {key: 'statusformtitleaccept', component: 'tool_policy'},
+ {key: 'iagreetothepolicy', component: 'tool_policy'},
+ {key: 'statusformtitlerevoke', component: 'tool_policy'},
+ {key: 'irevokethepolicy', component: 'tool_policy'},
+ {key: 'statusformtitledecline', component: 'tool_policy'},
+ {key: 'declinethepolicy', component: 'tool_policy'}
+ ]).then(function(strings) {
var title;
var saveText;
- if (action == 'revoke') {
- title = strings[4];
- saveText = strings[5];
- } else {
+ if (action == 'accept') {
title = strings[0];
saveText = strings[1];
+ } else if (action == 'revoke') {
+ title = strings[2];
+ saveText = strings[3];
+ } else if (action == 'decline') {
+ title = strings[4];
+ saveText = strings[5];
}
// Create the modal.
- ModalFactory.create({
+ return ModalFactory.create({
type: ModalFactory.types.SAVE_CANCEL,
title: title,
body: ''
this.setupFormModal(formData, saveText);
}.bind(this));
}.bind(this))
- .fail(Notification.exception);
+ .catch(Notification.exception);
};
/**
$filterstatus = $this->acceptancesfilter->get_status_filter();
if ($filterstatus == 1) {
$this->sql->from .= " $join AND a{$v}.status=1";
+ } else if ($filterstatus == 2) {
+ $this->sql->from .= " $join AND a{$v}.status=0";
} else {
$this->sql->from .= " LEFT $join";
}
- $this->sql->from .= " LEFT JOIN {user} m ON m.id = a{$v}.usermodified AND m.id <> u.id AND a{$v}.status = 1";
+ $this->sql->from .= " LEFT JOIN {user} m ON m.id = a{$v}.usermodified AND m.id <> u.id AND a{$v}.status IS NOT NULL";
$this->sql->params['versionid' . $v] = $v;
if ($filterstatus === 0) {
- $this->sql->where .= " AND (a{$v}.status IS NULL OR a{$v}.status = 0)";
+ $this->sql->where .= " AND a{$v}.status IS NULL";
}
- $this->add_column_header('status' . $v, get_string('agreed', 'tool_policy'), true, 'mdl-align');
- $this->add_column_header('timemodified', get_string('agreedon', 'tool_policy'));
- $this->add_column_header('usermodified' . $v, get_string('agreedby', 'tool_policy'));
+ $this->add_column_header('status' . $v, get_string('response', 'tool_policy'));
+ $this->add_column_header('timemodified', get_string('responseon', 'tool_policy'));
+ $this->add_column_header('usermodified' . $v, get_string('responseby', 'tool_policy'));
$this->add_column_header('note', get_string('acceptancenote', 'tool_policy'), false);
}
$join = "JOIN {tool_policy_acceptances} a{$v} ON a{$v}.userid = u.id AND a{$v}.policyversionid=:versionid{$v}";
if ($filterstatus == 1) {
$this->sql->from .= " {$join} AND a{$v}.status=1";
+ } else if ($filterstatus == 2) {
+ $this->sql->from .= " {$join} AND a{$v}.status=0";
} else {
$this->sql->from .= " LEFT {$join}";
}
$this->sql->params['versionid' . $v] = $v;
- $this->add_column_header('status' . $v, $versionname, true, 'mdl-align');
+ $this->add_column_header('status' . $v, $versionname);
$statusall[] = "COALESCE(a{$v}.status, 0)";
}
$this->sql->fields .= ",".join('+', $statusall)." AS statusall";
if ($filterstatus === 0) {
$statussql = [];
foreach ($this->versionids as $v => $versionname) {
- $statussql[] = "a{$v}.status IS NULL OR a{$v}.status = 0";
+ $statussql[] = "a{$v}.status IS NULL";
}
$this->sql->where .= " AND (u.policyagreed = 0 OR ".join(" OR ", $statussql).")";
}
echo \html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'returnurl',
'value' => $this->get_return_url()]);
foreach (array_keys($this->versionids) as $versionid) {
- echo \html_writer::empty_tag('input', ['type' => 'hidden', 'name' => "versionids[{$versionid}]",
+ echo \html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'versionids[]',
'value' => $versionid]);
}
}
public function wrap_html_finish() {
global $PAGE;
if ($this->canagreeany) {
+ echo \html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'action', 'value' => 'accept']);
echo \html_writer::empty_tag('input', ['type' => 'submit', 'data-action' => 'acceptmodal',
'value' => get_string('consentbulk', 'tool_policy'), 'class' => 'btn btn-primary m-t-1']);
$PAGE->requires->js_call_amd('tool_policy/acceptmodal', 'getInstance', [\context_system::instance()->id]);
$onbehalf = false;
$versions = $versionid ? [$versionid => $this->versionids[$versionid]] : $this->versionids; // List of versions.
$accepted = []; // List of versionids that user has accepted.
+ $declined = [];
foreach ($versions as $v => $name) {
- if (!empty($row->{'status' . $v})) {
- $accepted[] = $v;
+ if ($row->{'status' . $v} !== null) {
+ if (empty($row->{'status' . $v})) {
+ $declined[] = $v;
+ } else {
+ $accepted[] = $v;
+ }
$agreedby = $row->{'usermodified' . $v};
if ($agreedby && $agreedby != $row->id) {
$onbehalf = true;
}
}
- if ($versionid) {
- $str = new \lang_string($accepted ? 'yes' : 'no');
- } else {
- $str = new \lang_string('acceptancecount', 'tool_policy', (object)[
- 'agreedcount' => count($accepted),
- 'policiescount' => count($versions)
- ]);
- }
+ $ua = new user_agreement($row->id, $accepted, $declined, $this->get_return_url(), $versions, $onbehalf, $row->canaccept);
if ($this->is_downloading()) {
- return $str->out();
+ return $ua->export_for_download();
+
} else {
- $s = $this->output->render(new user_agreement($row->id, $accepted, $this->get_return_url(),
- $versions, $onbehalf, $row->canaccept));
- if (!$versionid) {
- $s .= '<br>' . \html_writer::link(new \moodle_url('/admin/tool/policy/user.php',
- ['userid' => $row->id, 'returnurl' => $this->get_return_url()]), $str);
- }
- return $s;
+ return $this->output->render($ua);
}
}
$policies = [];
$versions = [];
+ $optcache = \cache::make('tool_policy', 'policy_optional');
+
$rs = $DB->get_recordset_sql($sql, $params);
foreach ($rs as $r) {
}
$versions[$r->id][$versiondata->id] = $versiondata;
+
+ $optcache->set($versiondata->id, $versiondata->optional);
}
$rs->close();
return true;
}
- // Users have access to all the policies they have ever accepted.
- if (static::is_user_version_accepted($userid, $policy->id)) {
+ // Users have access to all the policies they have ever accepted/declined.
+ if (static::is_user_version_accepted($userid, $policy->id) !== null) {
return true;
}
}
/**
- * Returns version acceptance for this user.
+ * Did the user accept the given policy version?
*
* @param int $userid User identifier.
* @param int $versionid Policy version identifier.
- * @param array|null $acceptances Iist of policy version acceptances indexed by versionid.
- * @return bool True if this user has accepted this policy version; false otherwise.
+ * @param array|null $acceptances Pre-loaded list of policy version acceptances indexed by versionid.
+ * @return bool|null True/false if this user accepted/declined the policy; null otherwise.
*/
public static function is_user_version_accepted($userid, $versionid, $acceptances = null) {
+
$acceptance = static::get_user_version_acceptance($userid, $versionid, $acceptances);
+
if (!empty($acceptance)) {
- return $acceptance->status;
+ return (bool) $acceptance->status;
}
- return false;
+ return null;
}
/**
if (isset($acceptances[$policy->currentversion->id])) {
$policy->currentversion->acceptance = $acceptances[$policy->currentversion->id];
} else {
- $policy->currentversion->acceptance = 0;
+ $policy->currentversion->acceptance = null;
}
$versions[] = $policy->currentversion;
}
foreach ($policy->archivedversions as $version) {
if ($version->audience != policy_version::AUDIENCE_GUESTS
&& static::can_user_view_policy_version($version, $userid)) {
- $version->acceptance = isset($acceptances[$version->id]) ? $acceptances[$version->id] : 0;
+ $version->acceptance = isset($acceptances[$version->id]) ? $acceptances[$version->id] : null;
$versions[] = $version;
}
}
}
/**
- * Checks if user can accept policies for themselves or on behalf of another user
+ * Check if given policies can be accepted by the current user (eventually on behalf of the other user)
*
- * @param int $userid
- * @param bool $throwexception
+ * Currently, the version ids are not relevant and the check is based on permissions only. In the future, additional
+ * conditions can be added (such as policies applying to certain users only).
+ *
+ * @param array $versionids int[] List of policy version ids to check
+ * @param int $userid Accepting policies on this user's behalf (defaults to accepting on self)
+ * @param bool $throwexception Throw exception instead of returning false
* @return bool
*/
- public static function can_accept_policies($userid = null, $throwexception = false) {
+ public static function can_accept_policies(array $versionids, $userid = null, $throwexception = false) {
global $USER;
+
if (!isloggedin() || isguestuser()) {
if ($throwexception) {
throw new \moodle_exception('noguest');
return false;
}
}
+
if (!$userid) {
$userid = $USER->id;
}
}
/**
- * Checks if user can revoke policies for themselves or on behalf of another user
+ * Check if given policies can be declined by the current user (eventually on behalf of the other user)
*
- * @param int $userid
- * @param bool $throwexception
+ * Only optional policies can be declined. Otherwise, the permissions are same as for accepting policies.
+ *
+ * @param array $versionids int[] List of policy version ids to check
+ * @param int $userid Declining policies on this user's behalf (defaults to declining by self)
+ * @param bool $throwexception Throw exception instead of returning false
* @return bool
*/
- public static function can_revoke_policies($userid = null, $throwexception = false) {
+ public static function can_decline_policies(array $versionids, $userid = null, $throwexception = false) {
+
+ foreach ($versionids as $versionid) {
+ if (static::get_agreement_optional($versionid) == policy_version::AGREEMENT_COMPULSORY) {
+ // Compulsory policies can't be declined (that is what makes them compulsory).
+ if ($throwexception) {
+ throw new \moodle_exception('errorpolicyversioncompulsory', 'tool_policy');
+ } else {
+ return false;
+ }
+ }
+ }
+
+ return static::can_accept_policies($versionids, $userid, $throwexception);
+ }
+
+ /**
+ * Check if acceptances to given policies can be revoked by the current user (eventually on behalf of the other user)
+ *
+ * Revoking optional policies is controlled by the same rules as declining them. Compulsory policies can be revoked
+ * only by users with the permission to accept policies on other's behalf. The reasoning behind this is to make sure
+ * the user communicates with the site's privacy officer and is well aware of all consequences of the decision (such
+ * as losing right to access the site).
+ *
+ * @param array $versionids int[] List of policy version ids to check
+ * @param int $userid Revoking policies on this user's behalf (defaults to revoking by self)
+ * @param bool $throwexception Throw exception instead of returning false
+ * @return bool
+ */
+ public static function can_revoke_policies(array $versionids, $userid = null, $throwexception = false) {
global $USER;
+ // Guests' acceptance is not stored so there is nothing to revoke.
if (!isloggedin() || isguestuser()) {
if ($throwexception) {
throw new \moodle_exception('noguest');
return false;
}
}
- if (!$userid) {
- $userid = $USER->id;
+
+ // Sort policies into two sets according the optional flag.
+ $compulsory = [];
+ $optional = [];
+
+ foreach ($versionids as $versionid) {
+ $agreementoptional = static::get_agreement_optional($versionid);
+ if ($agreementoptional == policy_version::AGREEMENT_COMPULSORY) {
+ $compulsory[] = $versionid;
+ } else if ($agreementoptional == policy_version::AGREEMENT_OPTIONAL) {
+ $optional[] = $versionid;
+ } else {
+ throw new \coding_exception('Unexpected optional flag value');
+ }
}
- // At the moment, current users can't revoke their own policies.
- // Check capability to revoke on behalf as the real user.
- $realuser = manager::get_realuser();
- $usercontext = \context_user::instance($userid);
- if ($throwexception) {
- require_capability('tool/policy:acceptbehalf', $usercontext, $realuser);
- return;
- } else {
- return has_capability('tool/policy:acceptbehalf', $usercontext, $realuser);
+ // Check if the user can revoke the optional policies from the list.
+ if ($optional) {
+ if (!static::can_decline_policies($optional, $userid, $throwexception)) {
+ return false;
+ }
}
+
+ // Check if the user can revoke the compulsory policies from the list.
+ if ($compulsory) {
+ if (!$userid) {
+ $userid = $USER->id;
+ }
+
+ $realuser = manager::get_realuser();
+ $usercontext = \context_user::instance($userid);
+ if ($throwexception) {
+ require_capability('tool/policy:acceptbehalf', $usercontext, $realuser);
+ return;
+ } else {
+ return has_capability('tool/policy:acceptbehalf', $usercontext, $realuser);
+ }
+ }
+
+ return true;
}
/**
- * Accepts the current revisions of all policies that the user has not yet accepted
+ * Mark the given policy versions as accepted by the user.
*
- * @param array|int $policyversionid
- * @param int|null $userid
- * @param string|null $note
- * @param string|null $lang
+ * @param array|int $policyversionid Policy version id(s) to set acceptance status for.
+ * @param int|null $userid Id of the user accepting the policy version, defaults to the current one.
+ * @param string|null $note Note to be recorded.
+ * @param string|null $lang Language in which the policy was shown, defaults to the current one.
*/
public static function accept_policies($policyversionid, $userid = null, $note = null, $lang = null) {
+ static::set_acceptances_status($policyversionid, $userid, $note, $lang, 1);
+ }
+
+ /**
+ * Mark the given policy versions as declined by the user.
+ *
+ * @param array|int $policyversionid Policy version id(s) to set acceptance status for.
+ * @param int|null $userid Id of the user accepting the policy version, defaults to the current one.
+ * @param string|null $note Note to be recorded.
+ * @param string|null $lang Language in which the policy was shown, defaults to the current one.
+ */
+ public static function decline_policies($policyversionid, $userid = null, $note = null, $lang = null) {
+ static::set_acceptances_status($policyversionid, $userid, $note, $lang, 0);
+ }
+
+ /**
+ * Mark the given policy versions as accepted or declined by the user.
+ *
+ * @param array|int $policyversionid Policy version id(s) to set acceptance status for.
+ * @param int|null $userid Id of the user accepting the policy version, defaults to the current one.
+ * @param string|null $note Note to be recorded.
+ * @param string|null $lang Language in which the policy was shown, defaults to the current one.
+ * @param int $status The acceptance status, defaults to 1 = accepted
+ */
+ protected static function set_acceptances_status($policyversionid, $userid = null, $note = null, $lang = null, $status = 1) {
global $DB, $USER;
+
// Validate arguments and capabilities.
if (empty($policyversionid)) {
return;
if (!$userid) {
$userid = $USER->id;
}
- self::can_accept_policies($userid, true);
+ self::can_accept_policies([$policyversionid], $userid, true);
// Retrieve the list of policy versions that need agreement (do not update existing agreements).
list($sql, $params) = $DB->get_in_or_equal($policyversionid, SQL_PARAMS_NAMED);
$sql = "SELECT v.id AS versionid, a.*
FROM {tool_policy_versions} v
- LEFT JOIN {tool_policy_acceptances} a ON a.userid = :userid AND a.policyversionid = v.id
- WHERE (a.id IS NULL or a.status <> 1) AND v.id " . $sql;
- $needacceptance = $DB->get_records_sql($sql, ['userid' => $userid] + $params);
+ LEFT JOIN {tool_policy_acceptances} a ON a.userid = :userid AND a.policyversionid = v.id
+ WHERE v.id $sql AND (a.id IS NULL OR a.status <> :status)";
+
+ $needacceptance = $DB->get_records_sql($sql, $params + [
+ 'userid' => $userid,
+ 'status' => $status,
+ ]);
$realuser = manager::get_realuser();
- $updatedata = ['status' => 1, 'lang' => $lang ?: current_language(),
+ $updatedata = ['status' => $status, 'lang' => $lang ?: current_language(),
'timemodified' => time(), 'usermodified' => $realuser->id, 'note' => $note];
foreach ($needacceptance as $versionid => $currentacceptance) {
unset($currentacceptance->versionid);
$user = $DB->get_record('user', ['id' => $user], 'id, policyagreed');
}
- $sql = "SELECT d.id, a.status
+ $sql = "SELECT d.id, v.optional, a.status
FROM {tool_policy} d
- INNER JOIN {tool_policy_versions} v ON v.policyid = d.id AND v.id = d.currentversionid
- LEFT JOIN {tool_policy_acceptances} a ON a.userid = :userid AND a.policyversionid = v.id
- WHERE (v.audience = :audience OR v.audience = :audienceall)";
+ INNER JOIN {tool_policy_versions} v ON v.policyid = d.id AND v.id = d.currentversionid
+ LEFT JOIN {tool_policy_acceptances} a ON a.userid = :userid AND a.policyversionid = v.id
+ WHERE (v.audience = :audience OR v.audience = :audienceall)";
+
$params = [
'audience' => policy_version::AUDIENCE_LOGGEDIN,
'audienceall' => policy_version::AUDIENCE_ALL,
'userid' => $user->id
];
- $policies = $DB->get_records_sql_menu($sql, $params);
- $acceptedpolicies = array_filter($policies);
- $policyagreed = (count($policies) == count($acceptedpolicies)) ? 1 : 0;
- if ($user->policyagreed != $policyagreed) {
- $user->policyagreed = $policyagreed;
- $DB->set_field('user', 'policyagreed', $policyagreed, ['id' => $user->id]);
+ $allresponded = true;
+ foreach ($DB->get_records_sql($sql, $params) as $policyacceptance) {
+ if ($policyacceptance->optional == policy_version::AGREEMENT_COMPULSORY && empty($policyacceptance->status)) {
+ $allresponded = false;
+ } else if ($policyacceptance->optional == policy_version::AGREEMENT_OPTIONAL && $policyacceptance->status === null) {
+ $allresponded = false;
+ }
+ }
+
+ if ($user->policyagreed != $allresponded) {
+ $user->policyagreed = $allresponded;
+ $DB->set_field('user', 'policyagreed', $allresponded, ['id' => $user->id]);
}
}
if (!$userid) {
$userid = $USER->id;
}
- self::can_accept_policies($userid, true);
+ self::can_accept_policies([$policyversionid], $userid, true);
if ($currentacceptance = $DB->get_record('tool_policy_acceptances',
['policyversionid' => $policyversionid, 'userid' => $userid])) {
* @param \core\event\user_created $event
*/
public static function create_acceptances_user_created(\core\event\user_created $event) {
- global $CFG, $DB;
+ global $USER, $CFG, $DB;
// Do nothing if not set as the site policies handler.
if (empty($CFG->sitepolicyhandler) || $CFG->sitepolicyhandler !== 'tool_policy') {
if (!$user->policyagreed) {
return;
}
- // Remove the presignup cache after the user account is created.
+
+ // Cleanup our bits in the presignup cache (we can not rely on them at this stage any more anyway).
$cache = \cache::make('core', 'presignup');
$cache->delete('tool_policy_userpolicyagreed');
$cache->delete('tool_policy_viewedpolicies');
+ $cache->delete('tool_policy_policyversionidsagreed');
- // Get all active policies.
- $currentpolicyversions = static::get_current_versions_ids(policy_version::AUDIENCE_LOGGEDIN);
- // Save active policies as accepted by the user.
- if (!empty($currentpolicyversions)) {
+ // Mark all compulsory policies as implicitly accepted during the signup.
+ if ($policyversions = static::list_current_versions(policy_version::AUDIENCE_LOGGEDIN)) {
$acceptances = array();
- foreach ($currentpolicyversions as $policy) {
+ $now = time();
+ foreach ($policyversions as $policyversion) {
+ if ($policyversion->optional == policy_version::AGREEMENT_OPTIONAL) {
+ continue;
+ }
$acceptances[] = array(
- 'policyversionid' => $policy,
+ 'policyversionid' => $policyversion->id,
'userid' => $userid,
'status' => 1,
'lang' => $lang,
- 'usermodified' => 0,
- 'timecreated' => time(),
- 'timemodified' => time()
+ 'usermodified' => isset($USER->id) ? $USER->id : 0,
+ 'timecreated' => $now,
+ 'timemodified' => $now,
);
}
$DB->insert_records('tool_policy_acceptances', $acceptances);
}
+
+ static::update_policyagreed($userid);
+ }
+
+ /**
+ * Returns the value of the optional flag for the given policy version.
+ *
+ * Optimised for being called multiple times by making use of a request cache. The cache is normally populated as a
+ * side effect of calling {@link self::list_policies()} and in most cases should be warm enough for hits.
+ *
+ * @param int $versionid
+ * @return int policy_version::AGREEMENT_COMPULSORY | policy_version::AGREEMENT_OPTIONAL
+ */
+ public static function get_agreement_optional($versionid) {
+ global $DB;
+
+ $optcache = \cache::make('tool_policy', 'policy_optional');
+
+ $hit = $optcache->get($versionid);
+
+ if ($hit === false) {
+ $flags = $DB->get_records_menu('tool_policy_versions', null, '', 'id, optional');
+ $optcache->set_many($flags);
+ $hit = $flags[$versionid];
+ }
+
+ return $hit;
}
}
* Defines the form fields.
*/
public function definition() {
- global $PAGE;
+ global $PAGE, $USER;
$mform = $this->_form;
if (empty($this->_customdata['userids']) || !is_array($this->_customdata['userids'])) {
if (empty($this->_customdata['versionids']) || !is_array($this->_customdata['versionids'])) {
throw new \moodle_exception('missingparam', '', '', 'versionids');
}
- $revoke = (!empty($this->_customdata['action']) && $this->_customdata['action'] == 'revoke');
+ $action = $this->_customdata['action'];
$userids = clean_param_array($this->_customdata['userids'], PARAM_INT);
$versionids = clean_param_array($this->_customdata['versionids'], PARAM_INT);
- $usernames = $this->validate_and_get_users($userids, $revoke);
+ $usernames = $this->validate_and_get_users($versionids, $userids, $action);
$versionnames = $this->validate_and_get_versions($versionids);
foreach ($usernames as $userid => $name) {
get_string('policydochdrpolicy', 'tool_policy');
$mform->addElement('static', 'policy', $policyacceptancelabel, join(', ', $versionnames));
- if ($revoke) {
+ if ($action === 'revoke') {
$mform->addElement('static', 'ack', '', get_string('revokeacknowledgement', 'tool_policy'));
$mform->addElement('hidden', 'action', 'revoke');
- $mform->setType('action', PARAM_ALPHA);
- } else {
+ } else if ($action === 'accept') {
$mform->addElement('static', 'ack', '', get_string('acceptanceacknowledgement', 'tool_policy'));
+ $mform->addElement('hidden', 'action', 'accept');
+ } else if ($action === 'decline') {
+ $mform->addElement('static', 'ack', '', get_string('declineacknowledgement', 'tool_policy'));
+ $mform->addElement('hidden', 'action', 'decline');
+ } else {
+ throw new \moodle_exception('invalidaccessparameter');
+ }
+
+ $mform->setType('action', PARAM_ALPHA);
+
+ if (count($usernames) == 1 && isset($usernames[$USER->id])) {
+ // No need to display the acknowledgement if the users are giving/revoking acceptance on their own.
+ $mform->removeElement('ack');
}
$mform->addElement('textarea', 'note', get_string('acceptancenote', 'tool_policy'));
$mform->setType('note', PARAM_NOTAGS);
if (!empty($this->_customdata['showbuttons'])) {
- if ($revoke) {
+ if ($action === 'revoke') {
$this->add_action_buttons(true, get_string('irevokethepolicy', 'tool_policy'));
- } else {
+ } else if ($action === 'accept') {
$this->add_action_buttons(true, get_string('iagreetothepolicy', 'tool_policy'));
+ } else if ($action === 'decline') {
+ $this->add_action_buttons(true, get_string('declinethepolicy', 'tool_policy'));
}
}
/**
* Validate userids and return usernames
*
+ * @param array $versionids int[] List of policy version ids to process.
* @param array $userids
- * @param boolean $revoke True if policies will be revoked; false when policies will be accepted.
+ * @param string $action accept|decline|revoke
* @return array (userid=>username)
*/
- protected function validate_and_get_users($userids, $revoke = false) {
+ protected function validate_and_get_users($versionids, $userids, $action) {
global $DB;
+
$usernames = [];
list($sql, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
$params['usercontextlevel'] = CONTEXT_USER;
throw new \moodle_exception('noguest');
}
\context_helper::preload_from_record($user);
- if ($revoke) {
- api::can_revoke_policies($userid, true);
- } else {
- api::can_accept_policies($userid, true);
+ if ($action === 'revoke') {
+ api::can_revoke_policies($versionids, $userid, true);
+ } else if ($action === 'accept') {
+ api::can_accept_policies($versionids, $userid, true);
+ } else if ($action === 'decline') {
+ api::can_decline_policies($versionids, $userid, true);
}
$usernames[$userid] = fullname($user);
}
*/
public function process() {
if ($data = $this->get_data()) {
- $revoke = (!empty($data->action) && $data->action == 'revoke');
foreach ($data->userids as $userid) {
- if ($revoke) {
+ if ($data->action === 'revoke') {
foreach ($data->versionids as $versionid) {
\tool_policy\api::revoke_acceptance($versionid, $userid, $data->note);
}
- } else {
+ } else if ($data->action === 'accept') {
\tool_policy\api::accept_policies($data->versionids, $userid, $data->note);
+ } else if ($data->action === 'decline') {
+ \tool_policy\api::decline_policies($data->versionids, $userid, $data->note);
}
}
}
$mform->addElement('selectyesno', 'agreementstyle', get_string('policypriorityagreement', 'tool_policy'));
+ $mform->addElement('selectyesno', 'optional', get_string('policydocoptional', 'tool_policy'));
+
if (!$formdata->id || $formdata->status == policy_version::STATUS_DRAFT) {
// Creating a new version or editing a draft/archived version.
$mform->addElement('hidden', 'minorchange');
/** @var moodle_url */
protected $returnurl;
- /** @var bool */
- protected $canrevoke;
-
/**
* Contructor.
*
public function __construct($userid, $returnurl = null) {
$this->userid = $userid;
$this->returnurl = $returnurl ? (new moodle_url($returnurl))->out(false) : null;
- $this->canrevoke = \tool_policy\api::can_revoke_policies($this->userid);
}
/**
$data->hasonbehalfagreements = false;
$data->pluginbaseurl = (new moodle_url('/admin/tool/policy'))->out(false);
$data->returnurl = $this->returnurl;
- $data->canrevoke = $this->canrevoke;
// Get the list of policies and versions that current user is able to see
// and the respective acceptance records for the selected user.
$policies = api::get_policies_with_acceptances($this->userid);
+ $versionids = [];
$canviewfullnames = has_capability('moodle/site:viewfullnames', \context_system::instance());
foreach ($policies as $policy) {
-
foreach ($policy->versions as $version) {
+ $versionids[$version->id] = $version->id;
unset($version->summary);
unset($version->content);
$version->iscurrent = ($version->status == policy_version::STATUS_ACTIVE);
+ $version->isoptional = ($version->optional == policy_version::AGREEMENT_OPTIONAL);
$version->name = $version->name;
$version->revision = $version->revision;
$returnurl = new moodle_url('/admin/tool/policy/user.php', ['userid' => $this->userid]);
'returnurl' => $returnurl->out(false),
]))->out(false);
- if (!empty($version->acceptance->status)) {
+ if ($version->acceptance !== null) {
$acceptance = $version->acceptance;
$version->timeaccepted = userdate($acceptance->timemodified, get_string('strftimedatetime'));
$onbehalf = $acceptance->usermodified && $acceptance->usermodified != $this->userid;
- $version->agreement = new user_agreement($this->userid, [$version->id], $returnurl,
- [$version->id => $version->name], $onbehalf);
+ if ($version->acceptance->status == 1) {
+ $version->agreement = new user_agreement($this->userid, [$version->id], [], $returnurl,
+ [$version->id => $version->name], $onbehalf);
+ } else {
+ $version->agreement = new user_agreement($this->userid, [], [$version->id], $returnurl,
+ [$version->id => $version->name], $onbehalf);
+ }
if ($onbehalf) {
$usermodified = (object)['id' => $acceptance->usermodified];
username_load_fields_from_object($usermodified, $acceptance, 'mod');
}
$version->note = format_text($acceptance->note);
} else if ($version->iscurrent) {
- $version->agreement = new user_agreement($this->userid, [], $returnurl, [$version->id => $version->name]);
+ $version->agreement = new user_agreement($this->userid, [], [], $returnurl, [$version->id => $version->name]);
}
if (isset($version->agreement)) {
$version->agreement = $version->agreement->export_for_template($output);
}
$data->policies = array_values($policies);
+ $data->canrevoke = \tool_policy\api::can_revoke_policies(array_keys($versionids), $this->userid);
+
return $data;
}
}
switch ((int)$parts[1]) {
case self::FILTER_POLICYID:
case self::FILTER_VERSIONID:
- $value = (int)$parts[2];
- break;
case self::FILTER_CAPABILITY_ACCEPT:
case self::FILTER_STATUS:
- $value = (int)(bool)$parts[2];
+ $value = (int)$parts[2];
break;
case self::FILTER_ROLE:
$value = (int)$parts[2];
// Status.
$statuses = [
+ self::FILTER_STATUS.':2' => get_string('filterstatusdeclined', 'tool_policy'),
self::FILTER_STATUS.':1' => get_string('filterstatusyes', 'tool_policy'),
- self::FILTER_STATUS.':0' => get_string('filterstatusno', 'tool_policy'),
+ self::FILTER_STATUS.':0' => get_string('filterstatuspending', 'tool_policy'),
];
if (($currentstatus = $this->get_status_filter()) !== null) {
$selectedoptions[] = $key = self::FILTER_STATUS . ':' . $currentstatus;
/** @var array $agreedocs List of policy identifiers which the user has agreed using the form. */
protected $agreedocs = null;
+ /** @var array $declinedocs List of policy identifiers that the user declined. */
+ protected $declinedocs = null;
+
/** @var string $action Form action to identify when user agreeds policies. */
protected $action = null;
*
* @param array $listdocs List of policy version ids that were displayed to the user to agree with.
* @param array $agreedocs List of policy version ids that the user actually agreed with.
+ * @param array $declinedocs List of policy version ids that the user declined.
* @param int $behalfid The userid to accept the policy versions as (such as child's id).
* @param string $action Form action to identify when user agreeds policies.
*/
- public function __construct(array $listdocs, array $agreedocs = [], $behalfid = 0, $action = null) {
+ public function __construct(array $listdocs, array $agreedocs = [], array $declinedocs = [], $behalfid = 0, $action = null) {
global $USER;
$realuser = manager::get_realuser();
$this->listdocs = $listdocs;
$this->agreedocs = $agreedocs;
+ $this->declinedocs = $declinedocs;
$this->action = $action;
$this->isexistinguser = isloggedin() && !isguestuser();
}
$this->policies = api::list_current_versions(policy_version::AUDIENCE_LOGGEDIN);
+
+ if (!$this->isexistinguser) {
+ // During the signup, show compulsory policies only.
+ foreach ($this->policies as $ix => $policyversion) {
+ if ($policyversion->optional == policy_version::AGREEMENT_OPTIONAL) {
+ unset($this->policies[$ix]);
+ }
+ }
+ $this->policies = array_values($this->policies);
+ }
+
if (empty($this->behalfid)) {
$userid = $USER->id;
} else {
$userid = $this->behalfid;
}
+
$this->accept_and_revoke_policies();
$this->prepare_global_page_access($userid);
$this->prepare_user_acceptances($userid);
if ($this->isexistinguser) {
// Existing user.
if (!empty($this->action) && confirm_sesskey()) {
- // The form has been sent. Update policies acceptances according to $this->agreedocs.
+ // The form has been sent, update policies acceptances.
$lang = current_language();
// Accept / revoke policies.
- $acceptversionids = array();
+ $acceptversionids = [];
+ $declineversionids = [];
+
foreach ($this->policies as $policy) {
if (in_array($policy->id, $this->listdocs)) {
if (in_array($policy->id, $this->agreedocs)) {
- // Save policy version doc to accept it.
$acceptversionids[] = $policy->id;
+ } else if (in_array($policy->id, $this->declinedocs)) {
+ $declineversionids[] = $policy->id;
} else {
- // If the policy was displayed but not agreed, revoke the eventually given acceptance.
+ // If the policy was displayed but not answered, revoke the eventually given acceptance.
api::revoke_acceptance($policy->id, $this->behalfid);
}
}
}
- // Accept all policy docs saved in $acceptversionids.
+
api::accept_policies($acceptversionids, $this->behalfid, null, $lang);
+ api::decline_policies($declineversionids, $this->behalfid, null, $lang);
+
// Show a message to let know the user he/she must agree all the policies.
- &