include:
# Run grunt/npm install on highest version ('node' is an alias for the latest node.js version.)
- php: 7.2
- env: DB=none TASK=GRUNT NVM_VERSION='8.9'
+ env: DB=none TASK=GRUNT NVM_VERSION='lts/carbon'
exclude:
# MySQL - it's just too slow.
// Don't belong to the modifier user.
// Remove data from role_assignments.
- if (empty($context)) {
- return;
- }
$DB->delete_records('role_assignments', ['contextid' => $context->id]);
}
/**
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
$message->courseid = $SITE->id;
$message->component = 'tool_dataprivacy';
$message->name = 'contactdataprotectionofficer';
- $message->userfrom = $requestedby;
+ $message->userfrom = $requestedby->id;
$message->replyto = $requestedby->email;
$message->replytoname = $requestedby->fullname;
$message->subject = $subject;
}
$privacymanager = new \core_privacy\manager();
+ $privacymanager->set_observer(new \tool_dataprivacy\manager_observer());
foreach ($this->get_context_levels() as $level) {
--- /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 \tool_dataprivacy\manager_observer.
+ *
+ * @package tool_dataprivacy
+ * @copyright 2018 Marina Glancy
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_dataprivacy;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * A failure observer for the \core_privacy\manager.
+ *
+ * @package tool_dataprivacy
+ * @copyright 2018 Marina Glancy
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class manager_observer implements \core_privacy\manager_observer {
+ /**
+ * Notifies all DPOs that an exception occurred.
+ *
+ * @param \Throwable $e
+ * @param string $component
+ * @param string $interface
+ * @param string $methodname
+ * @param array $params
+ */
+ public function handle_component_failure($e, $component, $interface, $methodname, array $params) {
+ // Get the list of the site Data Protection Officers.
+ $dpos = api::get_site_dpos();
+
+ $messagesubject = get_string('exceptionnotificationsubject', 'tool_dataprivacy');
+ $a = (object)[
+ 'fullmethodname' => \core_privacy\manager::get_provider_classname_for_component($component) . '::' . $methodname,
+ 'component' => $component,
+ 'message' => $e->getMessage(),
+ 'backtrace' => $e->getTraceAsString()
+ ];
+ $messagebody = get_string('exceptionnotificationbody', 'tool_dataprivacy', $a);
+
+ // Email the data request to the Data Protection Officer(s)/Admin(s).
+ foreach ($dpos as $dpo) {
+ $message = new \core\message\message();
+ $message->courseid = SITEID;
+ $message->component = 'tool_dataprivacy';
+ $message->name = 'notifyexceptions';
+ $message->userfrom = \core_user::get_noreply_user();
+ $message->subject = $messagesubject;
+ $message->fullmessageformat = FORMAT_HTML;
+ $message->notification = 1;
+ $message->userto = $dpo;
+ $message->fullmessagehtml = $messagebody;
+ $message->fullmessage = html_to_text($messagebody);
+
+ // Send message.
+ message_send($message);
+ }
+ }
+}
*/
public function get_registry_metadata() {
$manager = new \core_privacy\manager();
+ $manager->set_observer(new \tool_dataprivacy\manager_observer());
+
$pluginman = \core_plugin_manager::instance();
$contributedplugins = $this->get_contrib_list();
$metadata = $manager->get_metadata_for_components();
// Add the list of relevant contexts to the request, and mark all as pending approval.
$privacymanager = new \core_privacy\manager();
+ $privacymanager->set_observer(new \tool_dataprivacy\manager_observer());
+
$contextlistcollection = $privacymanager->get_contexts_for_userid($datarequest->get('userid'));
api::add_request_contexts_with_status($contextlistcollection, $requestid, contextlist_context::STATUS_PENDING);
// Export the data.
$manager = new \core_privacy\manager();
+ $manager->set_observer(new \tool_dataprivacy\manager_observer());
+
$exportedcontent = $manager->export_user_data($approvedclcollection);
$fs = get_file_storage();
// Delete the data.
$manager = new \core_privacy\manager();
+ $manager->set_observer(new \tool_dataprivacy\manager_observer());
+
$manager->delete_data_for_user($approvedclcollection);
}
$PAGE->set_context($context);
// If contactdataprotectionofficer is disabled, send the user back to the profile page, or the privacy policy page.
-if (!\tool_dataprivacy\api::can_contact_dpo()) {
- redirect($returnurl, get_string('contactdpoviaprivacypolicy', 'tool_dataprivacy'), \core\output\notification::NOTIFY_ERROR);
+// That is, unless you have sufficient capabilities to perform this on behalf of a user.
+if (!$manage && !\tool_dataprivacy\api::can_contact_dpo()) {
+ redirect($returnurl, get_string('contactdpoviaprivacypolicy', 'tool_dataprivacy'), 0, \core\output\notification::NOTIFY_ERROR);
}
$mform = new tool_dataprivacy_data_request_form($url->out(false), ['manage' => !empty($manage)]);
'email' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDIN + MESSAGE_DEFAULT_LOGGEDOFF,
]
],
+
+ // Notify Data Protection Officer about exceptions.
+ 'notifyexceptions' => [
+ 'defaults' => [
+ 'email' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDIN + MESSAGE_DEFAULT_LOGGEDOFF,
+ ],
+ 'capability' => 'tool/dataprivacy:managedatarequests'
+ ],
];
$string['errorrequestnotfound'] = 'Request not found';
$string['errorrequestnotwaitingforapproval'] = 'The request is not awaiting approval. Either it is not yet ready or it has already been processed.';
$string['errorsendingmessagetodpo'] = 'An error was encountered while trying to send a message to {$a}.';
+$string['exceptionnotificationsubject'] = "Exception occured while processing privacy data";
+$string['exceptionnotificationbody'] = "<p>Exception occured while calling <b>{\$a->fullmethodname}</b>.<br>This means that plugin <b>{\$a->component}</b> did not complete processing data. Below you can find exception information that can be passed to the plugin developer.</p><pre>{\$a->message}<br>\n\n{\$a->backtrace}</pre>";
$string['expiredretentionperiodtask'] = 'Expired retention period';
$string['expiry'] = 'Expiry';
$string['expandplugin'] = 'Expand and collapse plugin.';
$string['lawfulbases_help'] = 'Select at least one option that will serve as the lawful basis for processing personal data. For details on these lawful bases, please see <a href="https://gdpr-info.eu/art-6-gdpr/" target="_blank">GDPR Art. 6.1</a>';
$string['messageprovider:contactdataprotectionofficer'] = 'Data requests';
$string['messageprovider:datarequestprocessingresults'] = 'Data request processing results';
+$string['messageprovider:notifyexceptions'] = 'Data requests exceptions notifications';
$string['message'] = 'Message';
$string['messagelabel'] = 'Message:';
$string['moduleinstancename'] = '{$a->instancename} ({$a->modulename})';
--- /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/>.
+
+/**
+ * Tests for the manager observer.
+ *
+ * @package tool_dataprivacy
+ * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * API tests.
+ *
+ * @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_manager_observer_testcase extends advanced_testcase {
+
+ /**
+ * Helper to set andn return two users who are DPOs.
+ */
+ protected function setup_site_dpos() {
+ global $DB;
+ $this->resetAfterTest();
+
+ $generator = new testing_data_generator();
+ $u1 = $this->getDataGenerator()->create_user();
+ $u2 = $this->getDataGenerator()->create_user();
+
+ $context = context_system::instance();
+
+ // Give the manager role with the capability to manage data requests.
+ $managerroleid = $DB->get_field('role', 'id', array('shortname' => 'manager'));
+ assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $managerroleid, $context->id, true);
+
+ // Assign both users as manager.
+ role_assign($managerroleid, $u1->id, $context->id);
+ role_assign($managerroleid, $u2->id, $context->id);
+
+ // Only map the manager role to the DPO role.
+ set_config('dporoles', $managerroleid, 'tool_dataprivacy');
+
+ return \tool_dataprivacy\api::get_site_dpos();
+ }
+
+ /**
+ * Ensure that when users are configured as DPO, they are sent an message upon failure.
+ */
+ public function test_handle_component_failure() {
+ $this->resetAfterTest();
+
+ // Create another user who is not a DPO.
+ $this->getDataGenerator()->create_user();
+
+ // Create the DPOs.
+ $dpos = $this->setup_site_dpos();
+
+ $observer = new \tool_dataprivacy\manager_observer();
+
+ // Handle the failure, catching messages.
+ $mailsink = $this->redirectMessages();
+ $mailsink->clear();
+ $observer->handle_component_failure(new \Exception('error'), 'foo', 'bar', 'baz', ['foobarbaz', 'bum']);
+
+ // Messages should be sent to both DPOs only.
+ $this->assertEquals(2, $mailsink->count());
+
+ $messages = $mailsink->get_messages();
+ $messageusers = array_map(function($message) {
+ return $message->useridto;
+ }, $messages);
+
+ $this->assertEquals(array_keys($dpos), $messageusers, '', 0.0, 0, true);
+ }
+
+ /**
+ * Ensure that when no user is configured as DPO, the message is sent to admin instead.
+ */
+ public function test_handle_component_failure_no_dpo() {
+ $this->resetAfterTest();
+
+ // Create another user who is not a DPO or admin.
+ $this->getDataGenerator()->create_user();
+
+ $observer = new \tool_dataprivacy\manager_observer();
+
+ $mailsink = $this->redirectMessages();
+ $mailsink->clear();
+ $observer->handle_component_failure(new \Exception('error'), 'foo', 'bar', 'baz', ['foobarbaz', 'bum']);
+
+ // Messages should have been sent only to the admin.
+ $this->assertEquals(1, $mailsink->count());
+
+ $messages = $mailsink->get_messages();
+ $message = reset($messages);
+
+ $admin = \core_user::get_user_by_username('admin');
+ $this->assertEquals($admin->id, $message->useridto);
+ }
+}
defined('MOODLE_INTERNAL') || die;
-$plugin->version = 2018051400;
+$plugin->version = 2018051401;
$plugin->requires = 2018050800; // Moodle 3.5dev (Build 2018031600) and upwards.
$plugin->component = 'tool_dataprivacy';
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
* @return collection A listing of user data stored through this system.
*/
public static function get_metadata(collection $collection) : collection {
- $collection->add_database_table('log', [
+ $collection->add_database_table('logstore_standard_log', [
'eventname' => 'privacy:metadata:log:eventname',
'userid' => 'privacy:metadata:log:userid',
'relateduserid' => 'privacy:metadata:log:relateduserid',
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
$availablemods = core_plugin_manager::instance()->get_plugins_of_type('mod');
$coursemodules = array();
$appsupportedmodules = array('assign', 'book', 'chat', 'choice', 'data', 'feedback', 'folder', 'forum', 'glossary', 'imscp',
- 'label', 'lesson', 'lti', 'page', 'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki');
+ 'label', 'lesson', 'lti', 'page', 'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop');
+
foreach ($availablemods as $mod) {
if (in_array($mod->name, $appsupportedmodules)) {
$coursemodules['$mmCourseDelegate_mmaMod' . ucfirst($mod->name)] = $mod->displayname;
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
/** @var array Info or error messages to show. */
protected $messages = [];
+ /** @var bool This is an existing user (rather than non-loggedin/guest). */
+ protected $isexistinguser;
+
/**
* Prepare the page for rendering.
*
$this->action = $action;
+ $this->isexistinguser = isloggedin() && !isguestuser();
$behalfid = $behalfid ?: $USER->id;
if ($realuser->id != $behalfid) {
$this->behalfuser = core_user::get_user($behalfid, '*', MUST_EXIST);
protected function accept_and_revoke_policies() {
global $USER;
- if (!empty($USER->id)) {
+ if ($this->isexistinguser) {
// Existing user.
if (!empty($this->action) && confirm_sesskey()) {
// The form has been sent. Update policies acceptances according to $this->agreedocs.
* Before display the consent page, the user has to view all the still-non-accepted policy docs.
* This function checks if the non-accepted policy docs have been shown and redirect to them.
*
- * @param array $userid User identifier who wants to access to the consent page.
- * @param url $returnurl URL to return after shown the policy docs.
+ * @param int $userid User identifier who wants to access to the consent page.
+ * @param moodle_url $returnurl URL to return after shown the policy docs.
*/
protected function redirect_to_policies($userid, $returnurl = null) {
- global $USER;
-
- $acceptances = api::get_user_acceptances($userid);
$allpolicies = $this->policies;
- if (!empty($userid)) {
+ if ($this->isexistinguser) {
+ $acceptances = api::get_user_acceptances($userid);
foreach ($allpolicies as $policy) {
if (api::is_user_version_accepted($userid, $policy->id, $acceptances)) {
// If this version is accepted by the user, remove from the pending policies list.
}
/**
- * Redirect to $SESSION->wantsurl if defined or to $CFG->wwwroot if not.
+ * Redirect to signup page if defined or to $CFG->wwwroot if not.
*/
protected function redirect_to_previous_url() {
global $SESSION;
- if (!empty($SESSION->wantsurl)) {
- $returnurl = $SESSION->wantsurl;
- unset($SESSION->wantsurl);
+ if ($this->isexistinguser) {
+ // Existing user.
+ if (!empty($SESSION->wantsurl)) {
+ $returnurl = $SESSION->wantsurl;
+ unset($SESSION->wantsurl);
+ } else {
+ $returnurl = new moodle_url('/admin/tool/policy/user.php');
+ }
} else {
- $returnurl = (new moodle_url('/admin/tool/policy/user.php'))->out();
+ // Non-authenticated user.
+ $issignup = \cache::make('core', 'presignup')->get('tool_policy_issignup');
+ if ($issignup) {
+ // User came here from signup page - redirect back there.
+ $returnurl = new moodle_url('/login/signup.php');
+ \cache::make('core', 'presignup')->set('tool_policy_issignup', false);
+ } else {
+ // Guests should not be on this page unless it's part of signup - redirect home.
+ $returnurl = new moodle_url('/');
+ }
}
redirect($returnurl);
* @param int $userid
*/
protected function prepare_global_page_access($userid) {
- global $PAGE, $SESSION, $SITE, $USER;
+ global $PAGE, $SITE, $USER;
// Guest users or not logged users (but the users during the signup process) are not allowed to access to this page.
- $newsignupuser = !empty($SESSION->wantsurl) && strpos($SESSION->wantsurl, 'login/signup.php') !== false;
- if (isguestuser() || (empty($USER->id) && !$newsignupuser)) {
+ $newsignupuser = \cache::make('core', 'presignup')->get('tool_policy_issignup');
+ if (!$this->isexistinguser && !$newsignupuser) {
$this->redirect_to_previous_url();
}
// Check for correct user capabilities.
- if (!empty($USER->id)) {
+ if ($this->isexistinguser) {
// For existing users, it's needed to check if they have the capability for accepting policies.
api::can_accept_policies($this->behalfid, true);
} else {
// For new users, the behalfid parameter is ignored.
- if ($this->behalfid != $USER->id) {
+ if ($this->behalfid) {
redirect(new moodle_url('/admin/tool/policy/index.php'));
}
}
// If the current user has the $USER->policyagreed = 1 or $userpolicyagreed = 1
- // and $SESSION->wantsurl is defined, redirect to the return page.
- $hasagreedsignupuser = empty($USER->id) && $this->signupuserpolicyagreed;
+ // redirect to the return page.
+ $hasagreedsignupuser = !$this->isexistinguser && $this->signupuserpolicyagreed;
$hasagreedloggeduser = $USER->id == $userid && !empty($USER->policyagreed);
if (!is_siteadmin() && ($hasagreedsignupuser || $hasagreedloggeduser)) {
$this->redirect_to_previous_url();
}
$myparams = [];
- if (!empty($USER->id) && !empty($this->behalfid) && $this->behalfid != $USER->id) {
+ if ($this->isexistinguser && !empty($this->behalfid) && $this->behalfid != $USER->id) {
$myparams['userid'] = $this->behalfid;
}
$myurl = new moodle_url('/admin/tool/policy/index.php', $myparams);
global $USER;
// Get all the policy version acceptances for this user.
- $acceptances = api::get_user_acceptances($userid);
$lang = current_language();
foreach ($this->policies as $policy) {
// Get a link to display the full policy document.
$policymodal = html_writer::link($policy->url, $policy->name, $policyattributes);
// Check if this policy version has been agreed or not.
- if (!empty($userid)) {
+ if ($this->isexistinguser) {
// Existing user.
$versionagreed = false;
+ $acceptances = api::get_user_acceptances($userid);
$policy->versionacceptance = api::get_user_version_acceptance($userid, $policy->id, $acceptances);
if (!empty($policy->versionacceptance)) {
// The policy version has ever been agreed. Check if status = 1 to know if still is accepted.
* Export the page data for the mustache template.
*
* @param renderer_base $output renderer to be used to render the page elements.
- * @return stdClass
+ * @return \stdClass
*/
public function export_for_template(renderer_base $output) {
global $USER;
$myparams = [];
- if (!empty($USER->id) && !empty($this->behalfid) && $this->behalfid != $USER->id) {
+ if ($this->isexistinguser && !empty($this->behalfid) && $this->behalfid != $USER->id) {
$myparams['userid'] = $this->behalfid;
}
$data = (object) [
/**
* Return the fields which contain personal data.
*
- * @param collection $items A reference to the collection to use to store the metadata.
- * @return collection The updated collection of metadata items.
+ * @param collection $collection The initialised collection to add items to.
+ * @return collection A listing of user data stored through this system.
*/
- public static function get_metadata(collection $items) : collection {
- $items->add_database_table(
+ public static function get_metadata(collection $collection) : collection {
+ $collection->add_database_table(
'tool_policy_acceptances',
[
'policyversionid' => 'privacy:metadata:acceptances:policyversionid',
'privacy:metadata:acceptances'
);
- return $items;
+ $collection->add_database_table(
+ 'tool_policy_versions',
+ [
+ 'name' => 'privacy:metadata:versions:name',
+ 'type' => 'privacy:metadata:versions:type',
+ 'audience' => 'privacy:metadata:versions:audience',
+ 'archived' => 'privacy:metadata:versions:archived',
+ 'usermodified' => 'privacy:metadata:versions:usermodified',
+ 'timecreated' => 'privacy:metadata:versions:timecreated',
+ 'timemodified' => 'privacy:metadata:versions:timemodified',
+ 'policyid' => 'privacy:metadata:versions:policyid',
+ 'revision' => 'privacy:metadata:versions:revision',
+ 'summary' => 'privacy:metadata:versions:summary',
+ 'summaryformat' => 'privacy:metadata:versions:summaryformat',
+ 'content' => 'privacy:metadata:versions:content',
+ 'contentformat' => 'privacy:metadata:versions:contentformat',
+ ],
+ 'privacy:metadata:versions'
+ );
+
+ $collection->add_subsystem_link('core_files', [], 'privacy:metadata:subsystem:corefiles');
+
+ return $collection;
}
/**
*/
public static function get_contexts_for_userid(int $userid) : contextlist {
$contextlist = new contextlist();
- $contextlist->add_from_sql('SELECT DISTINCT c.id
- FROM {tool_policy_acceptances} a
- JOIN {context} c ON a.userid = c.instanceid AND c.contextlevel = ?
- WHERE a.userid = ? OR a.usermodified = ?',
- [CONTEXT_USER, $userid, $userid]);
+
+ // Policies a user has modified.
+ $sql = "SELECT c.id
+ FROM {context} c
+ JOIN {tool_policy_versions} v ON v.usermodified = :userid
+ WHERE c.contextlevel = :contextlevel";
+ $params = [
+ 'contextlevel' => CONTEXT_SYSTEM,
+ 'userid' => $userid,
+ ];
+ $contextlist->add_from_sql($sql, $params);
+
+ // Policies a user has accepted.
+ $sql = "SELECT c.id
+ FROM {context} c
+ JOIN {tool_policy_acceptances} a ON c.instanceid = a.userid
+ WHERE
+ c.contextlevel = :contextlevel
+ AND (
+ a.userid = :userid OR a.usermodified = :usermodified
+ )";
+ $params = [
+ 'contextlevel' => CONTEXT_USER,
+ 'userid' => $userid,
+ 'usermodified' => $userid,
+ ];
+ $contextlist->add_from_sql($sql, $params);
+
return $contextlist;
}
*/
public static function export_user_data(approved_contextlist $contextlist) {
global $DB;
+
+ // Export user agreements.
foreach ($contextlist->get_contexts() as $context) {
- if ($context->contextlevel != CONTEXT_USER) {
- continue;
- }
- $user = $contextlist->get_user();
- $agreements = $DB->get_records_sql('SELECT a.id, a.userid, v.name, v.revision, a.usermodified, a.timecreated,
- a.timemodified, a.note, v.archived, p.currentversionid, a.status, a.policyversionid
- FROM {tool_policy_acceptances} a
- JOIN {tool_policy_versions} v ON v.id=a.policyversionid
- JOIN {tool_policy} p ON v.policyid = p.id
- WHERE a.userid = ? AND (a.userid = ? OR a.usermodified = ?)
- ORDER BY a.userid, v.archived, v.timecreated DESC',
- [$context->instanceid, $user->id, $user->id]);
- foreach ($agreements as $agreement) {
- $context = \context_user::instance($agreement->userid);
- $subcontext = [
- get_string('userpoliciesagreements', 'tool_policy'),
- transform::user($agreement->userid)
- ];
- $name = 'policyagreement-' . $agreement->policyversionid;
- $agreementcontent = (object) [
- 'userid' => transform::user($agreement->userid),
- 'status' => $agreement->status,
- 'versionid' => $agreement->policyversionid,
- 'name' => $agreement->name,
- 'revision' => $agreement->revision,
- 'isactive' => transform::yesno($agreement->policyversionid == $agreement->currentversionid),
- 'usermodified' => transform::user($agreement->usermodified),
- 'timecreated' => transform::datetime($agreement->timecreated),
- 'timemodified' => transform::datetime($agreement->timemodified),
- 'note' => $agreement->note,
- ];
- writer::with_context($context)->export_related_data($subcontext, $name, $agreementcontent);
+ if ($context->contextlevel == CONTEXT_USER) {
+ static::export_policy_agreements_for_context($context);
+ } else if ($context->contextlevel == CONTEXT_SYSTEM) {
+ static::export_authored_policies($contextlist->get_user());
}
}
}
* Delete all data for all users in the specified context.
*
* We never delete user agreements to the policies because they are part of privacy data.
+ * We never delete policy versions because they are part of privacy data.
*
* @param \context $context The context to delete in.
*/
* Delete all user data for the specified user, in the specified contexts.
*
* We never delete user agreements to the policies because they are part of privacy data.
+ * We never delete policy versions because they are part of privacy data.
*
* @param approved_contextlist $contextlist A list of contexts approved for deletion.
*/
public static function delete_data_for_user(approved_contextlist $contextlist) {
}
+
+ /**
+ * Export all policy agreements relating to the specified user context.
+ *
+ * @param \context_user $context The context to export
+ */
+ protected static function export_policy_agreements_for_context(\context_user $context) {
+ global $DB;
+
+ $sysctx = \context_system::instance();
+ $fs = get_file_storage();
+ $agreementsql = "
+ SELECT
+ a.id AS agreementid, a.userid, a.timemodified, a.note, a.status,
+ a.policyversionid AS versionid, a.usermodified, a.timecreated,
+ v.id, v.archived, v.name, v.revision,
+ v.summary, v.summaryformat,
+ v.content, v.contentformat,
+ p.currentversionid
+ FROM {tool_policy_acceptances} a
+ JOIN {tool_policy_versions} v ON v.id = a.policyversionid
+ JOIN {tool_policy} p ON v.policyid = p.id
+ WHERE a.userid = :userid OR a.usermodified = :usermodified";
+
+ // Fetch all agreements related to this user.
+ $agreements = $DB->get_recordset_sql($agreementsql, [
+ 'userid' => $context->instanceid,
+ 'usermodified' => $context->instanceid,
+ ]);
+
+ $basecontext = [
+ get_string('privacyandpolicies', 'admin'),
+ get_string('useracceptances', 'tool_policy'),
+ ];
+
+ foreach ($agreements as $agreement) {
+ $subcontext = array_merge($basecontext, [get_string('policynamedversion', 'tool_policy', $agreement)]);
+
+ $summary = writer::with_context($context)->rewrite_pluginfile_urls(
+ $subcontext,
+ 'tool_policy',
+ 'policydocumentsummary',
+ $agreement->versionid,
+ $agreement->summary
+ );
+ $content = writer::with_context($context)->rewrite_pluginfile_urls(
+ $subcontext,
+ 'tool_policy',
+ 'policydocumentcontent',
+ $agreement->versionid,
+ $agreement->content
+ );
+ $agreementcontent = (object) [
+ 'name' => $agreement->name,
+ 'revision' => $agreement->revision,
+ 'isactive' => transform::yesno($agreement->versionid == $agreement->currentversionid),
+ 'isagreed' => transform::yesno($agreement->status),
+ 'agreedby' => transform::user($agreement->usermodified),
+ 'timecreated' => transform::datetime($agreement->timecreated),
+ 'timemodified' => transform::datetime($agreement->timemodified),
+ 'note' => $agreement->note,
+ 'summary' => format_text($summary, $agreement->summaryformat),
+ 'content' => format_text($content, $agreement->contentformat),
+ ];
+
+ writer::with_context($context)->export_data($subcontext, $agreementcontent);
+ // Manually export the files as they reside in the system context so we can't use
+ // the write's helper methods.
+ foreach ($fs->get_area_files($sysctx->id, 'tool_policy', 'policydocumentsummary', $agreement->versionid) as $file) {
+ writer::with_context($context)->export_file($subcontext, $file);
+ }
+ foreach ($fs->get_area_files($sysctx->id, 'tool_policy', 'policydocumentcontent', $agreement->versionid) as $file) {
+ writer::with_context($context)->export_file($subcontext, $file);
+ }
+ }
+ $agreements->close();
+ }
+
+ /**
+ * Export all policy agreements that the user authored.
+ *
+ * @param stdClass $user The user who has created the policies to export.
+ */
+ protected static function export_authored_policies(\stdClass $user) {
+ global $DB;
+
+ // Authored policies are exported against the system.
+ $context = \context_system::instance();
+ $basecontext = [
+ get_string('policydocuments', 'tool_policy'),
+ ];
+
+ $sql = "SELECT v.id,
+ v.name,
+ v.revision,
+ v.summary,
+ v.content,
+ v.archived,
+ v.usermodified,
+ v.timecreated,
+ v.timemodified,
+ p.currentversionid
+ FROM {tool_policy_versions} v
+ JOIN {tool_policy} p ON p.id = v.policyid
+ WHERE v.usermodified = :userid";
+ $versions = $DB->get_recordset_sql($sql, ['userid' => $user->id]);
+ foreach ($versions as $version) {
+ $subcontext = array_merge($basecontext, [get_string('policynamedversion', 'tool_policy', $version)]);
+
+ $versioncontent = (object) [
+ 'name' => $version->name,
+ 'revision' => $version->revision,
+ 'summary' => writer::with_context($context)->rewrite_pluginfile_urls(
+ $subcontext,
+ 'tool_policy',
+ 'policydocumentsummary',
+ $version->id,
+ $version->summary
+ ),
+ 'content' => writer::with_context($context)->rewrite_pluginfile_urls(
+ $subcontext,
+ 'tool_policy',
+ 'policydocumentcontent',
+ $version->id,
+ $version->content
+ ),
+ 'isactive' => transform::yesno($version->id == $version->currentversionid),
+ 'isarchived' => transform::yesno($version->archived),
+ 'createdbyme' => transform::yesno($version->usermodified == $user->id),
+ 'timecreated' => transform::datetime($version->timecreated),
+ 'timemodified' => transform::datetime($version->timemodified),
+ ];
+ writer::with_context($context)
+ ->export_data($subcontext, $versioncontent)
+ ->export_area_files($subcontext, 'tool_policy', 'policydocumentsummary', $version->id)
+ ->export_area_files($subcontext, 'tool_policy', 'policydocumentcontent', $version->id);
+ }
+ $versions->close();
+ }
}
$PAGE->set_url('/admin/tool/policy/index.php');
$PAGE->set_popup_notification_allowed(false);
-if (!empty($USER->id)) {
+if (isloggedin() && !isguestuser()) {
// Existing user.
$haspermissionagreedocs = api::can_accept_policies($behalfid);
} else {
$string['policydoctype1'] = 'Privacy policy';
$string['policydoctype2'] = 'Third parties policy';
$string['policydoctype99'] = 'Other policy';
+$string['policydocuments'] = 'Policy documents';
+$string['policynamedversion'] = 'Policy {$a->name} (version {$a->revision} - {$a->id})';
$string['policyversionacceptedinbehalf'] = 'Consent for this policy has been given on your behalf.';
$string['policyversionacceptedinotherlang'] = 'Consent for this policy version has been given in a different language.';
$string['previousversions'] = '{$a} previous versions';
-$string['privacy:metadata:acceptances'] = 'Information from policy agreements made by site users';
-$string['privacy:metadata:acceptances:policyversionid'] = 'The ID of the accepted version policy.';
-$string['privacy:metadata:acceptances:userid'] = 'The ID of the user who agreed to the policy.';
-$string['privacy:metadata:acceptances:status'] = 'The status of the agreement: 0 if not accepted; 1 otherwise.';
-$string['privacy:metadata:acceptances:lang'] = 'The current language displayed when the policy is accepted.';
-$string['privacy:metadata:acceptances:usermodified'] = 'The ID of the user accepting the policy, if made on behalf of another user.';
-$string['privacy:metadata:acceptances:timecreated'] = 'The time when the user agreed to the policy';
-$string['privacy:metadata:acceptances:timemodified'] = 'The time when the user modified their agreement';
-$string['privacy:metadata:acceptances:note'] = 'Any comments added by a user when giving consent on behalf of another user';
+$string['privacy:metadata:acceptances'] = 'Information about policy agreements made by users.';
+$string['privacy:metadata:acceptances:policyversionid'] = 'The version of the policy for which consent was given.';
+$string['privacy:metadata:acceptances:userid'] = 'The user for whom this policy agreement relates to.';
+$string['privacy:metadata:acceptances:status'] = 'The status of the agreement.';
+$string['privacy:metadata:acceptances:lang'] = 'The language used to display the policy when consent was given.';
+$string['privacy:metadata:acceptances:usermodified'] = 'The user who gave consent for the policy, if made on behalf of another user.';
+$string['privacy:metadata:acceptances:timecreated'] = 'The time when the user agreed to the policy.';
+$string['privacy:metadata:acceptances:timemodified'] = 'The time when the user updated their agreement.';
+$string['privacy:metadata:acceptances:note'] = 'Any comments added by a user when giving consent on behalf of another user.';
+$string['privacy:metadata:subsystem:corefiles'] = 'The policy tool stores files included in the summary and full policy.';
+$string['privacy:metadata:versions'] = 'Policy version information.';
+$string['privacy:metadata:versions:name'] = 'The name of the policy.';
+$string['privacy:metadata:versions:type'] = 'Policy type.';
+$string['privacy:metadata:versions:audience'] = 'The type of users required to give their consent.';
+$string['privacy:metadata:versions:archived'] = 'The policy status (active or inactive).';
+$string['privacy:metadata:versions:usermodified'] = 'The user who modified the policy.';
+$string['privacy:metadata:versions:timecreated'] = 'The time that this version of the policy was created.';
+$string['privacy:metadata:versions:timemodified'] = 'The time that this version of the policy was updated.';
+$string['privacy:metadata:versions:policyid'] = 'The policy that this version is associated with.';
+$string['privacy:metadata:versions:revision'] = 'The revision name of this version of the policy.';
+$string['privacy:metadata:versions:summary'] = 'The summary of this version of the policy.';
+$string['privacy:metadata:versions:summaryformat'] = 'The format of the summary field.';
+$string['privacy:metadata:versions:content'] = 'The content of this version of the policy.';
+$string['privacy:metadata:versions:contentformat'] = 'The format of the content field.';
$string['privacysettings'] = 'Privacy settings';
$string['readpolicy'] = 'Please read our {$a}';
$string['refertofullpolicytext'] = 'Please refer to the full {$a} if you would like to review the text.';
$string['useracceptancecount'] = '{$a->agreedcount} of {$a->userscount} ({$a->percent}%)';
$string['useracceptancecountna'] = 'N/A';
$string['useracceptances'] = 'User agreements';
-$string['userpoliciesagreements'] = 'User agreements to policies';
$string['userpolicysettings'] = 'Policies';
$string['usersaccepted'] = 'Agreements';
$string['viewarchived'] = 'View previous versions';
* Hooks redirection to policy acceptance pages before sign up.
*/
function tool_policy_pre_signup_requests() {
- global $CFG, $SESSION;
+ global $CFG;
// Do nothing if we are not set as the site policies handler.
if (empty($CFG->sitepolicyhandler) || $CFG->sitepolicyhandler !== 'tool_policy') {
$userpolicyagreed = cache::make('core', 'presignup')->get('tool_policy_userpolicyagreed');
if (!empty($policies) && !$userpolicyagreed) {
// Redirect to "Policy" pages for consenting before creating the user.
- $SESSION->wantsurl = (new \moodle_url('/login/signup.php'))->out();
+ cache::make('core', 'presignup')->set('tool_policy_issignup', 1);
redirect(new \moodle_url('/admin/tool/policy/index.php'));
}
}
And I should see "Policies and agreements"
And I should see "No permission to agree to the policies on behalf of this user"
And I should see "Sorry, you do not have the required permission to agree to the following policies on behalf of User 1"
+
+ Scenario: Accept policy on sign up as a guest, one policy
+ Given the following config values are set as admin:
+ | registerauth | email |
+ | passwordpolicy | 0 |
+ | sitepolicyhandler | tool_policy |
+ Given the following policies exist:
+ | Policy | Name | Revision | Content | Summary | Status |
+ | P1 | This site policy | | full text1 | short text1 | archived |
+ | P1 | This site policy | | full text2 | short text2 | active |
+ | P1 | This site policy | | full text3 | short text3 | draft |
+ And I am on site homepage
+ And I follow "Log in"
+ # First log in as a guest
+ And I press "Log in as a guest"
+ # Now sign up
+ And I follow "Log in"
+ When I press "Create new account"
+ Then I should see "This site policy"
+ And I should see "short text2"
+ And I should see "full text2"
+ And I press "Next"
+ And I should see "Please agree to the following policies"
+ And I should see "This site policy"
+ And I should see "short text2"
+ And I should not see "full text2"
+ And I set the field "I agree to the This site policy" to "1"
+ And I press "Next"
+ And I should not see "I understand and agree"
+ And I set the following fields to these values:
+ | Username | user1 |
+ | Password | user1 |
+ | Email address | user1@address.invalid |
+ | Email (again) | user1@address.invalid |
+ | First name | User1 |
+ | Surname | L1 |
+ And I press "Create my new account"
+ And I should see "Confirm your account"
+ And I should see "An email should have been sent to your address at user1@address.invalid"
+ And I confirm email for "user1"
+ And I should see "Thanks, User1 L1"
+ And I should see "Your registration has been confirmed"
+ And I open my profile in edit mode
+ And the field "First name" matches value "User1"
+ And I log out
+ # Confirm that user can login and browse the site.
+ And I log in as "user1"
+ And I follow "Profile" in the user menu
+ # User can see his own agreements in the profile.
+ And I follow "Policies and agreements"
+ And "Agreed" "icon" should exist in the "This site policy" "table_row"
+ And I log out
/** @var stdClass The user object. */
protected $user;
+ /** @var stdClass The manager user object. */
+ protected $manager;
+
/**
* Setup function. Will create a user.
*/
$generator = $this->getDataGenerator();
$this->user = $generator->create_user();
- }
- /**
- * Test for provider::get_metadata().
- */
- public function test_get_metadata() {
- $collection = new collection('tool_policy');
- $newcollection = provider::get_metadata($collection);
- $itemcollection = $newcollection->get_collection();
- $this->assertCount(1, $itemcollection);
-
- $table = reset($itemcollection);
- $this->assertEquals('tool_policy_acceptances', $table->get_name());
-
- $privacyfields = $table->get_privacy_fields();
- $this->assertArrayHasKey('policyversionid', $privacyfields);
- $this->assertArrayHasKey('userid', $privacyfields);
- $this->assertArrayHasKey('status', $privacyfields);
- $this->assertArrayHasKey('lang', $privacyfields);
- $this->assertArrayHasKey('usermodified', $privacyfields);
- $this->assertArrayHasKey('timecreated', $privacyfields);
- $this->assertArrayHasKey('timemodified', $privacyfields);
- $this->assertArrayHasKey('note', $privacyfields);
-
- $this->assertEquals('privacy:metadata:acceptances', $table->get_summary());
+ // Create manager user.
+ $this->manager = $generator->create_user();
+ $syscontext = context_system::instance();
+ $rolemanagerid = create_role('Policy manager', 'policymanager', 'Can manage policy documents');
+ assign_capability('tool/policy:managedocs', CAP_ALLOW, $rolemanagerid, $syscontext->id);
+ assign_capability('tool/policy:acceptbehalf', CAP_ALLOW, $rolemanagerid, $syscontext->id);
+ role_assign($rolemanagerid, $this->manager->id, $syscontext->id);
+ accesslib_clear_all_caches_for_unit_testing();
}
/**
global $CFG;
// When there are no policies or agreements context list is empty.
+ $contextlist = \tool_policy\privacy\provider::get_contexts_for_userid($this->manager->id);
+ $this->assertEmpty($contextlist);
$contextlist = \tool_policy\privacy\provider::get_contexts_for_userid($this->user->id);
$this->assertEmpty($contextlist);
// Create a policy.
- $this->setAdminUser();
+ $this->setUser($this->manager);
$CFG->sitepolicyhandler = 'tool_policy';
$policy = $this->add_policy();
api::make_current($policy->get('id'));
- // When there are no agreements context list is empty.
+ // After creating a policy, there should be manager context.
+ $contextlist = \tool_policy\privacy\provider::get_contexts_for_userid($this->manager->id);
+ $this->assertEquals(1, $contextlist->count());
+
+ // But when there are no agreements, user context list is empty.
$contextlist = \tool_policy\privacy\provider::get_contexts_for_userid($this->user->id);
$this->assertEmpty($contextlist);
$this->assertEquals(1, $contextlist->count());
}
- public function test_export_own_agreements() {
- global $CFG, $USER;
+ public function test_export_agreements() {
+ global $CFG;
+
+ $otheruser = $this->getDataGenerator()->create_user();
+ $otherusercontext = \context_user::instance($otheruser->id);
- // Create policies and agree to them as admin.
- $this->setAdminUser();
- $admin = fullclone($USER);
- $admincontext = \context_user::instance($admin->id);
+ // Create policies and agree to them as manager.
+ $this->setUser($this->manager);
+ $managercontext = \context_user::instance($this->manager->id);
+ $systemcontext = \context_system::instance();
+ $agreementsubcontext = [
+ get_string('privacyandpolicies', 'admin'),
+ get_string('useracceptances', 'tool_policy')
+ ];
+ $versionsubcontext = [
+ get_string('policydocuments', 'tool_policy')
+ ];
$CFG->sitepolicyhandler = 'tool_policy';
$policy1 = $this->add_policy();
api::make_current($policy1->get('id'));
// Request export for this user.
$contextlist = provider::get_contexts_for_userid($this->user->id);
+ $this->assertCount(1, $contextlist);
$this->assertEquals([$usercontext->id], $contextlist->get_contextids());
$approvedcontextlist = new approved_contextlist($this->user, 'tool_policy', [$usercontext->id]);
provider::export_user_data($approvedcontextlist);
- // User can not see admin's agreements but can see his own.
- $writer = writer::with_context($admincontext);
- $dataadmin = $writer->get_related_data([get_string('userpoliciesagreements', 'tool_policy'), $admin->id]);
- $this->assertEmpty($dataadmin);
+ // User can not see manager's agreements but can see his own.
+ $writer = writer::with_context($managercontext);
+ $this->assertFalse($writer->has_any_data());
$writer = writer::with_context($usercontext);
- $datauser = $writer->get_related_data([get_string('userpoliciesagreements', 'tool_policy'), $this->user->id]);
- $this->assertCount(2, (array) $datauser);
- $this->assertEquals($policy1->get('name'), $datauser['policyagreement-'.$policy1->get('id')]->name);
- $this->assertEquals($this->user->id, $datauser['policyagreement-'.$policy1->get('id')]->usermodified);
- $this->assertEquals($policy2->get('name'), $datauser['policyagreement-'.$policy2->get('id')]->name);
- $this->assertEquals($this->user->id, $datauser['policyagreement-'.$policy2->get('id')]->usermodified);
+ $this->assertTrue($writer->has_any_data());
+
+ // Test policy 1.
+ $subcontext = array_merge($agreementsubcontext, [get_string('policynamedversion', 'tool_policy', $policy1->to_record())]);
+ $datauser = $writer->get_data($subcontext);
+ $this->assertEquals($policy1->get('name'), $datauser->name);
+ $this->assertEquals($this->user->id, $datauser->agreedby);
+ $this->assertEquals(strip_tags($policy1->get('summary')), strip_tags($datauser->summary));
+ $this->assertEquals(strip_tags($policy1->get('content')), strip_tags($datauser->content));
+
+ // Test policy 2.
+ $subcontext = array_merge($agreementsubcontext, [get_string('policynamedversion', 'tool_policy', $policy2->to_record())]);
+ $datauser = $writer->get_data($subcontext);
+ $this->assertEquals($policy2->get('name'), $datauser->name);
+ $this->assertEquals($this->user->id, $datauser->agreedby);
+ $this->assertEquals(strip_tags($policy2->get('summary')), strip_tags($datauser->summary));
+ $this->assertEquals(strip_tags($policy2->get('content')), strip_tags($datauser->content));
}
- public function test_export_agreements_on_behalf() {
- global $CFG, $USER;
+ public function test_export_agreements_for_other() {
+ global $CFG;
+
+ $managercontext = \context_user::instance($this->manager->id);
+ $systemcontext = \context_system::instance();
+ $usercontext = \context_user::instance($this->user->id);
- // Create policies.
- $this->setAdminUser();
- $admin = fullclone($USER);
+ // Create policies and agree to them as manager.
+ $this->setUser($this->manager);
+ $agreementsubcontext = [
+ get_string('privacyandpolicies', 'admin'),
+ get_string('useracceptances', 'tool_policy')
+ ];
+ $versionsubcontext = [
+ get_string('policydocuments', 'tool_policy')
+ ];
$CFG->sitepolicyhandler = 'tool_policy';
$policy1 = $this->add_policy();
api::make_current($policy1->get('id'));
$policy2 = $this->add_policy();
api::make_current($policy2->get('id'));
-
- // Agree to the policies for oneself and for another user.
- $usercontext = \context_user::instance($this->user->id);
- $admincontext = \context_user::instance($USER->id);
api::accept_policies([$policy1->get('id'), $policy2->get('id')]);
- api::accept_policies([$policy1->get('id'), $policy2->get('id')], $this->user->id, 'Mynote');
- // Request export for this user.
- $contextlist = provider::get_contexts_for_userid($this->user->id);
- $this->assertEquals([$usercontext->id], $contextlist->get_contextids());
-
- $writer = writer::with_context($usercontext);
- $this->assertFalse($writer->has_any_data());
+ // Agree to the other user's policies.
+ api::accept_policies([$policy1->get('id'), $policy2->get('id')], $this->user->id, 'My note');
+
+ // Request export for the manager.
+ $contextlist = provider::get_contexts_for_userid($this->manager->id);
+ $this->assertCount(3, $contextlist);
+ $this->assertEquals(
+ [$managercontext->id, $usercontext->id, $systemcontext->id],
+ $contextlist->get_contextids(),
+ '',
+ 0.0,
+ 1,
+ true
+ );
$approvedcontextlist = new approved_contextlist($this->user, 'tool_policy', [$usercontext->id]);
provider::export_user_data($approvedcontextlist);
- // User can not see admin's agreements but can see his own.
- $writer = writer::with_context($admincontext);
- $dataadmin = $writer->get_related_data([get_string('userpoliciesagreements', 'tool_policy'), $admin->id]);
- $this->assertEmpty($dataadmin);
+ // The user context has data.
+ $writer = writer::with_context($usercontext);
+ $this->assertTrue($writer->has_any_data());
+ // Test policy 1.
$writer = writer::with_context($usercontext);
- $datauser = $writer->get_related_data([get_string('userpoliciesagreements', 'tool_policy'), $this->user->id]);
- $this->assertCount(2, (array) $datauser);
- $this->assertEquals($policy1->get('name'), $datauser['policyagreement-'.$policy1->get('id')]->name);
- $this->assertEquals($admin->id, $datauser['policyagreement-'.$policy1->get('id')]->usermodified);
- $this->assertEquals('Mynote', $datauser['policyagreement-'.$policy1->get('id')]->note);
- $this->assertEquals($policy2->get('name'), $datauser['policyagreement-'.$policy2->get('id')]->name);
- $this->assertEquals($admin->id, $datauser['policyagreement-'.$policy2->get('id')]->usermodified);
- $this->assertEquals('Mynote', $datauser['policyagreement-'.$policy2->get('id')]->note);
-
- // Request export for the admin.
- writer::reset();
- $contextlist = provider::get_contexts_for_userid($USER->id);
- $this->assertEquals([$admincontext->id, $usercontext->id], $contextlist->get_contextids(), '', 0.0, 10, true);
-
- $approvedcontextlist = new approved_contextlist($USER, 'tool_policy', $contextlist->get_contextids());
- provider::export_user_data($approvedcontextlist);
+ $subcontext = array_merge($agreementsubcontext, [get_string('policynamedversion', 'tool_policy', $policy1->to_record())]);
+ $datauser = $writer->get_data($subcontext);
+ $this->assertEquals($policy1->get('name'), $datauser->name);
+ $this->assertEquals($this->manager->id, $datauser->agreedby);
+ $this->assertEquals(strip_tags($policy1->get('summary')), strip_tags($datauser->summary));
+ $this->assertEquals(strip_tags($policy1->get('content')), strip_tags($datauser->content));
+
+ // Test policy 2.
+ $subcontext = array_merge($agreementsubcontext, [get_string('policynamedversion', 'tool_policy', $policy2->to_record())]);
+ $datauser = $writer->get_data($subcontext);
+ $this->assertEquals($policy2->get('name'), $datauser->name);
+ $this->assertEquals($this->manager->id, $datauser->agreedby);
+ $this->assertEquals(strip_tags($policy2->get('summary')), strip_tags($datauser->summary));
+ $this->assertEquals(strip_tags($policy2->get('content')), strip_tags($datauser->content));
+ }
- // Admin can see all four agreements.
- $writer = writer::with_context($admincontext);
- $dataadmin = $writer->get_related_data([get_string('userpoliciesagreements', 'tool_policy'), $admin->id]);
- $this->assertCount(2, (array) $dataadmin);
- $this->assertEquals($policy1->get('name'), $dataadmin['policyagreement-'.$policy1->get('id')]->name);
- $this->assertEquals($admin->id, $dataadmin['policyagreement-'.$policy1->get('id')]->usermodified);
- $this->assertEquals($policy2->get('name'), $dataadmin['policyagreement-'.$policy2->get('id')]->name);
- $this->assertEquals($admin->id, $dataadmin['policyagreement-'.$policy2->get('id')]->usermodified);
+ public function test_export_created_policies() {
+ global $CFG;
- $writer = writer::with_context($usercontext);
- $datauser = $writer->get_related_data([get_string('userpoliciesagreements', 'tool_policy'), $this->user->id]);
- $this->assertCount(2, (array) $datauser);
- $this->assertEquals($policy1->get('name'), $datauser['policyagreement-'.$policy1->get('id')]->name);
- $this->assertEquals($admin->id, $datauser['policyagreement-'.$policy1->get('id')]->usermodified);
- $this->assertEquals('Mynote', $datauser['policyagreement-'.$policy1->get('id')]->note);
- $this->assertEquals($policy2->get('name'), $datauser['policyagreement-'.$policy2->get('id')]->name);
- $this->assertEquals($admin->id, $datauser['policyagreement-'.$policy2->get('id')]->usermodified);
- $this->assertEquals('Mynote', $datauser['policyagreement-'.$policy2->get('id')]->note);
+ // Create policies and agree to them as manager.
+ $this->setUser($this->manager);
+ $managercontext = \context_user::instance($this->manager->id);
+ $systemcontext = \context_system::instance();
+ $agreementsubcontext = [
+ get_string('privacyandpolicies', 'admin'),
+ get_string('useracceptances', 'tool_policy')
+ ];
+ $versionsubcontext = [
+ get_string('policydocuments', 'tool_policy')
+ ];
+ $CFG->sitepolicyhandler = 'tool_policy';
+ $policy1 = $this->add_policy();
+ api::make_current($policy1->get('id'));
+ $policy2 = $this->add_policy();
+ api::make_current($policy2->get('id'));
+ api::accept_policies([$policy1->get('id'), $policy2->get('id')]);
+
+ // Agree to the policies for oneself.
+ $contextlist = provider::get_contexts_for_userid($this->manager->id);
+ $this->assertCount(2, $contextlist);
+ $this->assertEquals([$managercontext->id, $systemcontext->id], $contextlist->get_contextids(), '', 0.0, 1, true);
+
+ $approvedcontextlist = new approved_contextlist($this->manager, 'tool_policy', $contextlist->get_contextids());
+ provider::export_user_data($approvedcontextlist);
+
+ // User has agreed to policies.
+ $writer = writer::with_context($managercontext);
+ $this->assertTrue($writer->has_any_data());
+
+ // Test policy 1.
+ $subcontext = array_merge($agreementsubcontext, [get_string('policynamedversion', 'tool_policy', $policy1->to_record())]);
+ $datauser = $writer->get_data($subcontext);
+ $this->assertEquals($policy1->get('name'), $datauser->name);
+ $this->assertEquals($this->manager->id, $datauser->agreedby);
+ $this->assertEquals(strip_tags($policy1->get('summary')), strip_tags($datauser->summary));
+ $this->assertEquals(strip_tags($policy1->get('content')), strip_tags($datauser->content));
+
+ // Test policy 2.
+ $subcontext = array_merge($agreementsubcontext, [get_string('policynamedversion', 'tool_policy', $policy2->to_record())]);
+ $datauser = $writer->get_data($subcontext);
+ $this->assertEquals($policy2->get('name'), $datauser->name);
+ $this->assertEquals($this->manager->id, $datauser->agreedby);
+ $this->assertEquals(strip_tags($policy2->get('summary')), strip_tags($datauser->summary));
+ $this->assertEquals(strip_tags($policy2->get('content')), strip_tags($datauser->content));
+
+ // User can see policy documents.
+ $writer = writer::with_context($systemcontext);
+ $this->assertTrue($writer->has_any_data());
+
+ $subcontext = array_merge($versionsubcontext, [get_string('policynamedversion', 'tool_policy', $policy1->to_record())]);
+ $dataversion = $writer->get_data($subcontext);
+ $this->assertEquals($policy1->get('name'), $dataversion->name);
+ $this->assertEquals(get_string('yes'), $dataversion->createdbyme);
+
+ $subcontext = array_merge($versionsubcontext, [get_string('policynamedversion', 'tool_policy', $policy2->to_record())]);
+ $dataversion = $writer->get_data($subcontext);
+ $this->assertEquals($policy2->get('name'), $dataversion->name);
+ $this->assertEquals(get_string('yes'), $dataversion->createdbyme);
}
/**
global $DB;
// Grab the course category context.
- $context = \context_coursecat::instance($this->_categoryid);
-
- // Delete the files.
- $fs = get_file_storage();
- $files = $fs->get_area_files($context->id, 'tool_recyclebin', TOOL_RECYCLEBIN_COURSECAT_BIN_FILEAREA, $item->id);
- foreach ($files as $file) {
- $file->delete();
+ $context = \context_coursecat::instance($this->_categoryid, IGNORE_MISSING);
+ if (!empty($context)) {
+ // Delete the files.
+ $fs = get_file_storage();
+ $fs->delete_area_files($context->id, 'tool_recyclebin', TOOL_RECYCLEBIN_COURSECAT_BIN_FILEAREA, $item->id);
+ } else {
+ // Course category has been deleted. Find records using $item->id as this is unique for coursecat recylebin.
+ $files = $DB->get_recordset('files', [
+ 'component' => 'tool_recyclebin',
+ 'filearea' => TOOL_RECYCLEBIN_COURSECAT_BIN_FILEAREA,
+ 'itemid' => $item->id,
+ ]);
+ $fs = get_file_storage();
+ foreach ($files as $filer) {
+ $file = $fs->get_file_instance($filer);
+ $file->delete();
+ }
+ $files->close();
}
// Delete the record.
'id' => $item->id
));
+ // The coursecat might have been deleted, check we have a context before triggering event.
+ if (!$context) {
+ return;
+ }
+
// Fire event.
$event = \tool_recyclebin\event\category_bin_item_deleted::create(array(
'objectid' => $item->id,
global $DB;
// Grab the course context.
- $context = \context_course::instance($this->_courseid);
-
- // Delete the files.
- $fs = get_file_storage();
- $files = $fs->get_area_files($context->id, 'tool_recyclebin', TOOL_RECYCLEBIN_COURSE_BIN_FILEAREA, $item->id);
- foreach ($files as $file) {
- $file->delete();
+ $context = \context_course::instance($this->_courseid, IGNORE_MISSING);
+
+ if (!empty($context)) {
+ // Delete the files.
+ $fs = get_file_storage();
+ $fs->delete_area_files($context->id, 'tool_recyclebin', TOOL_RECYCLEBIN_COURSE_BIN_FILEAREA, $item->id);
+ } else {
+ // Course context has been deleted. Find records using $item->id as this is unique for course bin recyclebin.
+ $files = $DB->get_recordset('files', [
+ 'component' => 'tool_recyclebin',
+ 'filearea' => TOOL_RECYCLEBIN_COURSE_BIN_FILEAREA,
+ 'itemid' => $item->id,
+ ]);
+ $fs = get_file_storage();
+ foreach ($files as $filer) {
+ $file = $fs->get_file_instance($filer);
+ $file->delete();
+ }
+ $files->close();
}
// Delete the record.
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
* @param \context $context The context to delete data for.
*/
public static function delete_data_for_all_users_in_context(\context $context) {
- if (empty($context)) {
- return;
- }
-
if ($context->contextlevel != CONTEXT_USER) {
return;
}
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
/**
* Usually same than major release zero version, mainly for informative/historic purposes.
*/
- const RELEASE = '3.5';
+ const RELEASE = '3.6';
/**
* Cipher to be used in backup and restore operations.
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
namespace block_recent_activity\privacy;
+use core_privacy\local\metadata\collection;
+use core_privacy\local\request\approved_contextlist;
+use core_privacy\local\request\contextlist;
+
defined('MOODLE_INTERNAL') || die();
/**
* @copyright 2018 Shamim Rezaie <shamim@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-class provider implements \core_privacy\local\metadata\null_provider {
+class provider implements \core_privacy\local\metadata\provider,
+ \core_privacy\local\request\plugin\provider {
+
+ /**
+ * Returns metadata.
+ *
+ * @param collection $collection The initialised collection to add items to.
+ * @return collection A listing of user data stored through this system.
+ */
+ public static function get_metadata(collection $collection) : collection {
+
+ // This plugin defines a db table but it is not considered personal data and, therefore, not exported or deleted.
+ $collection->add_database_table('block_recent_activity', [
+ 'courseid' => 'privacy:metadata:block_recent_activity:courseid',
+ 'cmid' => 'privacy:metadata:block_recent_activity:cmid',
+ 'timecreated' => 'privacy:metadata:block_recent_activity:timecreated',
+ 'userid' => 'privacy:metadata:block_recent_activity:userid',
+ 'action' => 'privacy:metadata:block_recent_activity:action',
+ 'modname' => 'privacy:metadata:block_recent_activity:modname'
+ ], 'privacy:metadata:block_recent_activity');
+
+ return $collection;
+ }
+
+ /**
+ * Get the list of contexts that contain user information for the specified user.
+ *
+ * @param int $userid The user to search.
+ * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
+ */
+ public static function get_contexts_for_userid(int $userid) : contextlist {
+ return new contextlist();
+ }
+
+ /**
+ * Export all user data for the specified user, in the specified contexts.
+ *
+ * @param approved_contextlist $contextlist The approved contexts to export information for.
+ */
+ public static function export_user_data(approved_contextlist $contextlist) {
+ }
+
+ /**
+ * Delete all data for all users in the specified context.
+ *
+ * @param \context $context The specific context to delete data for.
+ */
+ public static function delete_data_for_all_users_in_context(\context $context) {
+ }
/**
- * Get the language string identifier with the component's language
- * file to explain why this plugin stores no data.
+ * Delete all user data for the specified user, in the specified contexts.
*
- * @return string
+ * @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
*/
- public static function get_reason() : string {
- return 'privacy:metadata';
+ public static function delete_data_for_user(approved_contextlist $contextlist) {
}
}
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
$string['pluginname'] = 'Recent activity';
$string['privacy:metadata'] = 'The recent activity block contains a cache of data stored elsewhere in Moodle.';
+$string['privacy:metadata:block_recent_activity'] = 'Temporary log of recent teacher activity. Removed after two days';
+$string['privacy:metadata:block_recent_activity:action'] = 'Action: created, updated or deleted';
+$string['privacy:metadata:block_recent_activity:cmid'] = 'Course module id';
+$string['privacy:metadata:block_recent_activity:courseid'] = 'Course id';
+$string['privacy:metadata:block_recent_activity:modname'] = 'Module type name (for delete action)';
+$string['privacy:metadata:block_recent_activity:timecreated'] = 'Time when action was performed';
+$string['privacy:metadata:block_recent_activity:userid'] = 'User performing the action';
$string['recent_activity:addinstance'] = 'Add a new recent activity block';
$string['recent_activity:viewaddupdatemodule'] = 'View added and updated modules in recent activity block';
$string['recent_activity:viewdeletemodule'] = 'View deleted modules in recent activity block';
* @param approved_contextlist $contextlist The approved contexts to export information for.
*/
public static function export_user_data(approved_contextlist $contextlist) {
+ $rssdata = [];
$results = static::get_records($contextlist->get_user()->id);
foreach ($results as $result) {
- $data = (object) [
+ $rssdata[] = (object) [
'title' => $result->title,
'preferredtitle' => $result->preferredtitle,
'description' => $result->description,
'shared' => \core_privacy\local\request\transform::yesno($result->shared),
'url' => $result->url
];
-
+ }
+ if (!empty($rssdata)) {
+ $data = (object) [
+ 'feeds' => $rssdata,
+ ];
\core_privacy\local\request\writer::with_context($contextlist->current())->export_data([
get_string('pluginname', 'block_rss_client')], $data);
}
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
$user = $this->getDataGenerator()->create_user();
$context = context_user::instance($user->id);
+ $this->add_rss_feed($user);
$this->add_rss_feed($user);
$writer = \core_privacy\local\request\writer::with_context($context);
$this->export_context_data_for_user($user->id, $context, 'block_rss_client');
$data = $writer->get_data([get_string('pluginname', 'block_rss_client')]);
- $this->assertEquals('BBC News - World', $data->title);
- $this->assertEquals('World News', $data->preferredtitle);
- $this->assertEquals('Description: BBC News - World', $data->description);
- $this->assertEquals(get_string('no'), $data->shared);
- $this->assertEquals('http://feeds.bbci.co.uk/news/world/rss.xml?edition=uk', $data->url);
+ $this->assertCount(2, $data->feeds);
+ $feed1 = reset($data->feeds);
+ $this->assertEquals('BBC News - World', $feed1->title);
+ $this->assertEquals('World News', $feed1->preferredtitle);
+ $this->assertEquals('Description: BBC News - World', $feed1->description);
+ $this->assertEquals(get_string('no'), $feed1->shared);
+ $this->assertEquals('http://feeds.bbci.co.uk/news/world/rss.xml?edition=uk', $feed1->url);
}
/**
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
* @param context $context Transform the specific context to delete data for.
*/
public static function delete_data_for_all_users_in_context(\context $context) {
- if (empty($context)) {
- return;
- }
-
// Delete all Calendar Events in the specified context in batches.
if ($eventids = array_keys(self::get_calendar_event_ids_by_context($context))) {
self::delete_batch_records('event', 'id', $eventids);
* @param context $context A user context.
*/
public static function delete_data_for_all_users_in_context(\context $context) {
- if (empty($context)) {
- return;
- }
-
if (!$context instanceof \context_system && !$context instanceof \context_coursecat) {
return;
}
"require-dev": {
"phpunit/phpunit": "6.5.*",
"phpunit/dbUnit": "3.0.*",
- "moodlehq/behat-extension": "3.35.1",
+ "moodlehq/behat-extension": "3.36.0",
"mikey179/vfsStream": "^1.6"
}
}
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
- "content-hash": "f047772340a956bbf2c02d91536b11c6",
+ "content-hash": "956ce0b653b805efb6a9a483f8c9a847",
"packages": [],
"packages-dev": [
{
},
{
"name": "behat/mink-browserkit-driver",
- "version": "v1.3.2",
+ "version": "1.3.3",
"source": {
"type": "git",
"url": "https://github.com/minkphp/MinkBrowserKitDriver.git",
- "reference": "10e67fb4a295efcd62ea0bf16025a85ea19534fb"
+ "reference": "1b9a7ce903cfdaaec5fb32bfdbb26118343662eb"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/minkphp/MinkBrowserKitDriver/zipball/10e67fb4a295efcd62ea0bf16025a85ea19534fb",
- "reference": "10e67fb4a295efcd62ea0bf16025a85ea19534fb",
+ "url": "https://api.github.com/repos/minkphp/MinkBrowserKitDriver/zipball/1b9a7ce903cfdaaec5fb32bfdbb26118343662eb",
+ "reference": "1b9a7ce903cfdaaec5fb32bfdbb26118343662eb",
"shasum": ""
},
"require": {
"behat/mink": "^1.7.1@dev",
"php": ">=5.3.6",
- "symfony/browser-kit": "~2.3|~3.0",
- "symfony/dom-crawler": "~2.3|~3.0"
+ "symfony/browser-kit": "~2.3|~3.0|~4.0",
+ "symfony/dom-crawler": "~2.3|~3.0|~4.0"
},
"require-dev": {
- "silex/silex": "~1.2",
- "symfony/phpunit-bridge": "~2.7|~3.0"
+ "mink/driver-testsuite": "dev-master",
+ "symfony/http-kernel": "~2.3|~3.0|~4.0"
},
"type": "mink-driver",
"extra": {
"browser",
"testing"
],
- "time": "2016-03-05T08:59:47+00:00"
+ "time": "2018-05-02T09:25:31+00:00"
},
{
"name": "behat/mink-extension",
},
{
"name": "guzzlehttp/guzzle",
- "version": "6.3.0",
+ "version": "6.3.3",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
- "reference": "f4db5a78a5ea468d4831de7f0bf9d9415e348699"
+ "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/guzzle/zipball/f4db5a78a5ea468d4831de7f0bf9d9415e348699",
- "reference": "f4db5a78a5ea468d4831de7f0bf9d9415e348699",
+ "url": "https://api.github.com/repos/guzzle/guzzle/zipball/407b0cb880ace85c9b63c5f9551db498cb2d50ba",
+ "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba",
"shasum": ""
},
"require": {
},
"require-dev": {
"ext-curl": "*",
- "phpunit/phpunit": "^4.0 || ^5.0",
+ "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0",
"psr/log": "^1.0"
},
"suggest": {
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "6.2-dev"
+ "dev-master": "6.3-dev"
}
},
"autoload": {
"rest",
"web service"
],
- "time": "2017-06-22T18:50:49+00:00"
+ "time": "2018-04-22T15:46:56+00:00"
},
{
"name": "guzzlehttp/promises",
},
{
"name": "moodlehq/behat-extension",
- "version": "v3.35.1",
+ "version": "v3.36.0",
"source": {
"type": "git",
"url": "https://github.com/moodlehq/moodle-behat-extension.git",
- "reference": "e6e92fd551185f73603bad5694e854f3f6906e0e"
+ "reference": "ba8c4b8b323e05f7af128604f3f3dc60c953135a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/moodlehq/moodle-behat-extension/zipball/e6e92fd551185f73603bad5694e854f3f6906e0e",
- "reference": "e6e92fd551185f73603bad5694e854f3f6906e0e",
+ "url": "https://api.github.com/repos/moodlehq/moodle-behat-extension/zipball/ba8c4b8b323e05f7af128604f3f3dc60c953135a",
+ "reference": "ba8c4b8b323e05f7af128604f3f3dc60c953135a",
"shasum": ""
},
"require": {
},
"notification-url": "https://packagist.org/downloads/",
"license": [
- "GPLv3"
+ "GPL-3.0-or-later"
],
"authors": [
{
"Behat",
"moodle"
],
- "time": "2018-01-24T14:09:40+00:00"
+ "time": "2018-02-04T18:04:02+00:00"
},
{
"name": "myclabs/deep-copy",
},
{
"name": "phpspec/prophecy",
- "version": "1.7.5",
+ "version": "1.7.6",
"source": {
"type": "git",
"url": "https://github.com/phpspec/prophecy.git",
- "reference": "dfd6be44111a7c41c2e884a336cc4f461b3b2401"
+ "reference": "33a7e3c4fda54e912ff6338c48823bd5c0f0b712"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpspec/prophecy/zipball/dfd6be44111a7c41c2e884a336cc4f461b3b2401",
- "reference": "dfd6be44111a7c41c2e884a336cc4f461b3b2401",
+ "url": "https://api.github.com/repos/phpspec/prophecy/zipball/33a7e3c4fda54e912ff6338c48823bd5c0f0b712",
+ "reference": "33a7e3c4fda54e912ff6338c48823bd5c0f0b712",
"shasum": ""
},
"require": {
"doctrine/instantiator": "^1.0.2",
"php": "^5.3|^7.0",
"phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0",
- "sebastian/comparator": "^1.1|^2.0",
+ "sebastian/comparator": "^1.1|^2.0|^3.0",
"sebastian/recursion-context": "^1.0|^2.0|^3.0"
},
"require-dev": {
"spy",
"stub"
],
- "time": "2018-02-19T10:16:54+00:00"
+ "time": "2018-04-18T13:57:24+00:00"
},
{
"name": "phpunit/dbunit",
},
{
"name": "phpunit/php-code-coverage",
- "version": "5.3.0",
+ "version": "5.3.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
- "reference": "661f34d0bd3f1a7225ef491a70a020ad23a057a1"
+ "reference": "c89677919c5dd6d3b3852f230a663118762218ac"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/661f34d0bd3f1a7225ef491a70a020ad23a057a1",
- "reference": "661f34d0bd3f1a7225ef491a70a020ad23a057a1",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/c89677919c5dd6d3b3852f230a663118762218ac",
+ "reference": "c89677919c5dd6d3b3852f230a663118762218ac",
"shasum": ""
},
"require": {
"testing",
"xunit"
],
- "time": "2017-12-06T09:29:45+00:00"
+ "time": "2018-04-06T15:36:58+00:00"
},
{
"name": "phpunit/php-file-iterator",
},
{
"name": "phpunit/phpunit",
- "version": "6.5.7",
+ "version": "6.5.8",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "6bd77b57707c236833d2b57b968e403df060c9d9"
+ "reference": "4f21a3c6b97c42952fd5c2837bb354ec0199b97b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/6bd77b57707c236833d2b57b968e403df060c9d9",
- "reference": "6bd77b57707c236833d2b57b968e403df060c9d9",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4f21a3c6b97c42952fd5c2837bb354ec0199b97b",
+ "reference": "4f21a3c6b97c42952fd5c2837bb354ec0199b97b",
"shasum": ""
},
"require": {
"testing",
"xunit"
],
- "time": "2018-02-26T07:01:09+00:00"
+ "time": "2018-04-10T11:38:34+00:00"
},
{
"name": "phpunit/phpunit-mock-objects",
- "version": "5.0.6",
+ "version": "5.0.7",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git",
- "reference": "33fd41a76e746b8fa96d00b49a23dadfa8334cdf"
+ "reference": "3eaf040f20154d27d6da59ca2c6e28ac8fd56dce"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/33fd41a76e746b8fa96d00b49a23dadfa8334cdf",
- "reference": "33fd41a76e746b8fa96d00b49a23dadfa8334cdf",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/3eaf040f20154d27d6da59ca2c6e28ac8fd56dce",
+ "reference": "3eaf040f20154d27d6da59ca2c6e28ac8fd56dce",
"shasum": ""
},
"require": {
"mock",
"xunit"
],
- "time": "2018-01-06T05:45:45+00:00"
+ "time": "2018-05-29T13:50:43+00:00"
},
{
"name": "psr/container",
},
{
"name": "symfony/browser-kit",
- "version": "v3.4.6",
+ "version": "v3.4.11",
"source": {
"type": "git",
"url": "https://github.com/symfony/browser-kit.git",
- "reference": "490f27762705c8489bd042fe3e9377a191dba9b4"
+ "reference": "840bb6f0d5b3701fd768b68adf7193c2d0f98f79"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/browser-kit/zipball/490f27762705c8489bd042fe3e9377a191dba9b4",
- "reference": "490f27762705c8489bd042fe3e9377a191dba9b4",
+ "url": "https://api.github.com/repos/symfony/browser-kit/zipball/840bb6f0d5b3701fd768b68adf7193c2d0f98f79",
+ "reference": "840bb6f0d5b3701fd768b68adf7193c2d0f98f79",
"shasum": ""
},
"require": {
],
"description": "Symfony BrowserKit Component",
"homepage": "https://symfony.com",
- "time": "2018-01-03T07:37:34+00:00"
+ "time": "2018-03-19T22:32:39+00:00"
},
{
"name": "symfony/class-loader",
- "version": "v3.4.6",
+ "version": "v3.4.11",
"source": {
"type": "git",
"url": "https://github.com/symfony/class-loader.git",
},
{
"name": "symfony/config",
- "version": "v3.4.6",
+ "version": "v3.4.11",
"source": {
"type": "git",
"url": "https://github.com/symfony/config.git",
- "reference": "05e10567b529476a006b00746c5f538f1636810e"
+ "reference": "73e055cf2e6467715f187724a0347ea32079967c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/config/zipball/05e10567b529476a006b00746c5f538f1636810e",
- "reference": "05e10567b529476a006b00746c5f538f1636810e",
+ "url": "https://api.github.com/repos/symfony/config/zipball/73e055cf2e6467715f187724a0347ea32079967c",
+ "reference": "73e055cf2e6467715f187724a0347ea32079967c",
"shasum": ""
},
"require": {
"php": "^5.5.9|>=7.0.8",
- "symfony/filesystem": "~2.8|~3.0|~4.0"
+ "symfony/filesystem": "~2.8|~3.0|~4.0",
+ "symfony/polyfill-ctype": "~1.8"
},
"conflict": {
"symfony/dependency-injection": "<3.3",
],
"description": "Symfony Config Component",
"homepage": "https://symfony.com",
- "time": "2018-02-14T10:03:57+00:00"
+ "time": "2018-05-14T16:49:53+00:00"
},
{
"name": "symfony/console",
- "version": "v3.3.16",
+ "version": "v3.3.17",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
},
{
"name": "symfony/css-selector",
- "version": "v3.4.6",
+ "version": "v3.4.11",
"source": {
"type": "git",
"url": "https://github.com/symfony/css-selector.git",
- "reference": "544655f1fc078a9cd839fdda2b7b1e64627c826a"
+ "reference": "d2ce52290b648ae33b5301d09bc14ee378612914"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/css-selector/zipball/544655f1fc078a9cd839fdda2b7b1e64627c826a",
- "reference": "544655f1fc078a9cd839fdda2b7b1e64627c826a",
+ "url": "https://api.github.com/repos/symfony/css-selector/zipball/d2ce52290b648ae33b5301d09bc14ee378612914",
+ "reference": "d2ce52290b648ae33b5301d09bc14ee378612914",
"shasum": ""
},
"require": {
],
"description": "Symfony CssSelector Component",
"homepage": "https://symfony.com",
- "time": "2018-02-03T14:55:07+00:00"
+ "time": "2018-05-16T12:49:49+00:00"
},
{
"name": "symfony/debug",
- "version": "v3.4.6",
+ "version": "v3.4.11",
"source": {
"type": "git",
"url": "https://github.com/symfony/debug.git",
- "reference": "9b1071f86e79e1999b3d3675d2e0e7684268b9bc"
+ "reference": "b28fd73fefbac341f673f5efd707d539d6a19f68"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/debug/zipball/9b1071f86e79e1999b3d3675d2e0e7684268b9bc",
- "reference": "9b1071f86e79e1999b3d3675d2e0e7684268b9bc",
+ "url": "https://api.github.com/repos/symfony/debug/zipball/b28fd73fefbac341f673f5efd707d539d6a19f68",
+ "reference": "b28fd73fefbac341f673f5efd707d539d6a19f68",
"shasum": ""
},
"require": {
],
"description": "Symfony Debug Component",
"homepage": "https://symfony.com",
- "time": "2018-02-28T21:49:22+00:00"
+ "time": "2018-05-16T14:03:39+00:00"
},
{
"name": "symfony/dependency-injection",
- "version": "v3.3.16",
+ "version": "v3.3.17",
"source": {
"type": "git",
"url": "https://github.com/symfony/dependency-injection.git",
},
{
"name": "symfony/dom-crawler",
- "version": "v3.4.6",
+ "version": "v3.4.11",
"source": {
"type": "git",
"url": "https://github.com/symfony/dom-crawler.git",
- "reference": "2bb5d3101cc01f4fe580e536daf4f1959bc2d24d"
+ "reference": "201b210fafcdd193c1e45b2994bf7133fb6263e8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/2bb5d3101cc01f4fe580e536daf4f1959bc2d24d",
- "reference": "2bb5d3101cc01f4fe580e536daf4f1959bc2d24d",
+ "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/201b210fafcdd193c1e45b2994bf7133fb6263e8",
+ "reference": "201b210fafcdd193c1e45b2994bf7133fb6263e8",
"shasum": ""
},
"require": {
"php": "^5.5.9|>=7.0.8",
+ "symfony/polyfill-ctype": "~1.8",
"symfony/polyfill-mbstring": "~1.0"
},
"require-dev": {
],
"description": "Symfony DomCrawler Component",
"homepage": "https://symfony.com",
- "time": "2018-02-22T10:48:49+00:00"
+ "time": "2018-05-01T22:53:27+00:00"
},
{
"name": "symfony/event-dispatcher",
- "version": "v3.4.6",
+ "version": "v3.4.11",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher.git",
- "reference": "58990682ac3fdc1f563b7e705452921372aad11d"
+ "reference": "fdd5abcebd1061ec647089c6c41a07ed60af09f8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/58990682ac3fdc1f563b7e705452921372aad11d",
- "reference": "58990682ac3fdc1f563b7e705452921372aad11d",
+ "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/fdd5abcebd1061ec647089c6c41a07ed60af09f8",
+ "reference": "fdd5abcebd1061ec647089c6c41a07ed60af09f8",
"shasum": ""
},
"require": {
],
"description": "Symfony EventDispatcher Component",
"homepage": "https://symfony.com",
- "time": "2018-02-14T10:03:57+00:00"
+ "time": "2018-04-06T07:35:25+00:00"
},
{
"name": "symfony/filesystem",
- "version": "v3.4.6",
+ "version": "v3.4.11",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
- "reference": "253a4490b528597aa14d2bf5aeded6f5e5e4a541"
+ "reference": "8e03ca3fa52a0f56b87506f38cf7bd3f9442b3a0"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/filesystem/zipball/253a4490b528597aa14d2bf5aeded6f5e5e4a541",
- "reference": "253a4490b528597aa14d2bf5aeded6f5e5e4a541",
+ "url": "https://api.github.com/repos/symfony/filesystem/zipball/8e03ca3fa52a0f56b87506f38cf7bd3f9442b3a0",
+ "reference": "8e03ca3fa52a0f56b87506f38cf7bd3f9442b3a0",
"shasum": ""
},
"require": {
- "php": "^5.5.9|>=7.0.8"
+ "php": "^5.5.9|>=7.0.8",
+ "symfony/polyfill-ctype": "~1.8"
},
"type": "library",
"extra": {
],
"description": "Symfony Filesystem Component",
"homepage": "https://symfony.com",
- "time": "2018-02-22T10:48:49+00:00"
+ "time": "2018-05-16T08:49:21+00:00"
+ },
+ {
+ "name": "symfony/polyfill-ctype",
+ "version": "v1.8.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-ctype.git",
+ "reference": "7cc359f1b7b80fc25ed7796be7d96adc9b354bae"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/7cc359f1b7b80fc25ed7796be7d96adc9b354bae",
+ "reference": "7cc359f1b7b80fc25ed7796be7d96adc9b354bae",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.8-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Polyfill\\Ctype\\": ""
+ },
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ },
+ {
+ "name": "Gert de Pagter",
+ "email": "BackEndTea@gmail.com"
+ }
+ ],
+ "description": "Symfony polyfill for ctype functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "ctype",
+ "polyfill",
+ "portable"
+ ],
+ "time": "2018-04-30T19:57:29+00:00"
},
{
"name": "symfony/polyfill-mbstring",
- "version": "v1.7.0",
+ "version": "v1.8.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
- "reference": "78be803ce01e55d3491c1397cf1c64beb9c1b63b"
+ "reference": "3296adf6a6454a050679cde90f95350ad604b171"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/78be803ce01e55d3491c1397cf1c64beb9c1b63b",
- "reference": "78be803ce01e55d3491c1397cf1c64beb9c1b63b",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/3296adf6a6454a050679cde90f95350ad604b171",
+ "reference": "3296adf6a6454a050679cde90f95350ad604b171",
"shasum": ""
},
"require": {
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.7-dev"
+ "dev-master": "1.8-dev"
}
},
"autoload": {
"portable",
"shim"
],
- "time": "2018-01-30T19:27:44+00:00"
+ "time": "2018-04-26T10:06:28+00:00"
},
{
"name": "symfony/process",
- "version": "v2.8.36",
+ "version": "v2.8.41",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
- "reference": "756f614c5061729ea245ac6717231f7e3bfb74f9"
+ "reference": "713952f2ccbcc8342ecdbe1cb313d3e2da8aad28"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/process/zipball/756f614c5061729ea245ac6717231f7e3bfb74f9",
- "reference": "756f614c5061729ea245ac6717231f7e3bfb74f9",
+ "url": "https://api.github.com/repos/symfony/process/zipball/713952f2ccbcc8342ecdbe1cb313d3e2da8aad28",
+ "reference": "713952f2ccbcc8342ecdbe1cb313d3e2da8aad28",
"shasum": ""
},
"require": {
],
"description": "Symfony Process Component",
"homepage": "https://symfony.com",
- "time": "2018-02-12T17:44:58+00:00"
+ "time": "2018-05-15T21:17:45+00:00"
},
{
"name": "symfony/translation",
- "version": "v3.3.16",
+ "version": "v3.3.17",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
},
{
"name": "symfony/yaml",
- "version": "v3.3.16",
+ "version": "v3.3.17",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
* @param course_in_list $course The currently selected course.
* @param int $page The page being displayed.
* @param int $perpage The number of courses to display per page.
+ * @param string|null $viewmode The view mode the page is in, one out of 'default', 'combined', 'courses' or 'categories'.
* @return string
*/
- public function course_listing(coursecat $category = null, course_in_list $course = null, $page = 0, $perpage = 20) {
+ public function course_listing(coursecat $category = null, course_in_list $course = null, $page = 0, $perpage = 20,
+ $viewmode = 'default') {
if ($category === null) {
$html = html_writer::start_div('select-a-category');
$html .= html_writer::tag('h3', $category->get_formatted_name(),
array('id' => 'course-listing-title', 'tabindex' => '0'));
$html .= $this->course_listing_actions($category, $course, $perpage);
- $html .= $this->listing_pagination($category, $page, $perpage);
+ $html .= $this->listing_pagination($category, $page, $perpage, false, $viewmode);
$html .= html_writer::start_tag('ul', array('class' => 'ml-1 course-list', 'role' => 'group'));
foreach ($category->get_courses($options) as $listitem) {
$html .= $this->course_listitem($category, $listitem, $courseid);
}
$html .= html_writer::end_tag('ul');
- $html .= $this->listing_pagination($category, $page, $perpage, true);
+ $html .= $this->listing_pagination($category, $page, $perpage, true, $viewmode);
$html .= $this->course_bulk_actions($category);
$html .= html_writer::end_div();
return $html;
* @param int $page The current page.
* @param int $perpage The number of courses to display per page.
* @param bool $showtotals Set to true to show the total number of courses and what is being displayed.
+ * @param string|null $viewmode The view mode the page is in, one out of 'default', 'combined', 'courses' or 'categories'.
* @return string
*/
- protected function listing_pagination(coursecat $category, $page, $perpage, $showtotals = false) {
+ protected function listing_pagination(coursecat $category, $page, $perpage, $showtotals = false, $viewmode = 'default') {
$html = '';
$totalcourses = $category->get_courses_count();
$totalpages = ceil($totalcourses / $perpage);
}
}
$items = array();
- $baseurl = new moodle_url('/course/management.php', array('categoryid' => $category->id));
+ if ($viewmode !== 'default') {
+ $baseurl = new moodle_url('/course/management.php', array('categoryid' => $category->id,
+ 'view' => $viewmode));
+ } else {
+ $baseurl = new moodle_url('/course/management.php', array('categoryid' => $category->id));
+ }
if ($page > 0) {
$items[] = $this->action_button(new moodle_url($baseurl, array('page' => 0)), get_string('first'));
$items[] = $this->action_button(new moodle_url($baseurl, array('page' => $page - 1)), get_string('prev'));
$o .= $this->section_availability($section);
$o .= html_writer::start_tag('div', array('class' => 'summary'));
- $o .= $this->format_summary_text($section);
+ if ($section->uservisible || $section->visible) {
+ // Show summary if section is available or has availability restriction information.
+ // Do not show summary if section is hidden but we still display it because of course setting
+ // "Hidden sections are shown in collapsed form".
+ $o .= $this->format_summary_text($section);
+ }
$o .= html_writer::end_tag('div');
return $o;
}
/**
- * Generate a summary of a section for display on the 'coruse index page'
+ * Generate a summary of a section for display on the 'course index page'
*
* @param stdClass $section The course_section entry from DB
* @param stdClass $course The course entry from DB
}
$o .= $this->output->heading($title, 3, 'section-title');
+ $o .= $this->section_availability($section);
$o.= html_writer::start_tag('div', array('class' => 'summarytext'));
- $o.= $this->format_summary_text($section);
+
+ if ($section->uservisible || $section->visible) {
+ // Show summary if section is available or has availability restriction information.
+ // Do not show summary if section is hidden but we still display it because of course setting
+ // "Hidden sections are shown in collapsed form".
+ $o .= $this->format_summary_text($section);
+ }
$o.= html_writer::end_tag('div');
$o.= $this->section_activity_summary($section, $course, null);
- $o .= $this->section_availability($section);
-
$o .= html_writer::end_tag('div');
$o .= html_writer::end_tag('li');
if (!$section->visible) {
if ($canviewhidden) {
$o .= $this->courserenderer->availability_info(get_string('hiddenfromstudents'), 'ishidden');
+ } else {
+ // We are here because of the setting "Hidden sections are shown in collapsed form".
+ // Student can not see the section contents but can see its name.
+ $o .= $this->courserenderer->availability_info(get_string('notavailable'), 'ishidden');
}
} else if (!$section->uservisible) {
if ($section->availableinfo) {
* Show if something is on on the course clipboard (moving around)
*
* @param stdClass $course The course entry from DB
- * @param int $sectionno The section number in the coruse which is being dsiplayed
+ * @param int $sectionno The section number in the course which is being displayed
* @return string HTML to output.
*/
protected function course_activity_clipboard($course, $sectionno = null) {
*
* @param stdClass $course The course entry from DB
* @param array $sections The course_sections entries from the DB
- * @param int $sectionno The section number in the coruse which is being dsiplayed
+ * @param int $sectionno The section number in the course which is being displayed
* @return array associative array with previous and next section link
*/
protected function get_nav_links($course, $sections, $sectionno) {
/**
* Generate the header html of a stealth section
*
- * @param int $sectionno The section number in the coruse which is being dsiplayed
+ * @param int $sectionno The section number in the course which is being displayed
* @return string HTML to output.
*/
protected function stealth_section_header($sectionno) {
/**
* Generate the html for a hidden section
*
- * @param int $sectionno The section number in the coruse which is being dsiplayed
+ * @param int $sectionno The section number in the course which is being displayed
* @param int|stdClass $courseorid The course to get the section name for (object or just course id)
* @return string HTML to output.
*/
$course = course_get_format($course)->get_course();
// Can we view the section in question?
- if (!($sectioninfo = $modinfo->get_section_info($displaysection))) {
- // This section doesn't exist
- print_error('unknowncoursesection', 'error', null, $course->fullname);
- return;
- }
-
- if (!$sectioninfo->uservisible) {
- if (!$course->hiddensections) {
- echo $this->start_section_list();
- echo $this->section_hidden($displaysection, $course->id);
- echo $this->end_section_list();
- }
- // Can't view this section.
- return;
+ if (!($sectioninfo = $modinfo->get_section_info($displaysection)) || !$sectioninfo->uservisible) {
+ // This section doesn't exist or is not available for the user.
+ // We actually already check this in course/view.php but just in case exit from this function as well.
+ print_error('unknowncoursesection', 'error', course_get_url($course),
+ format_string($course->fullname));
}
// Copy activity clipboard..
continue;
}
// Show the section if the user is permitted to access it, OR if it's not available
- // but there is some available info text which explains the reason & should display.
+ // but there is some available info text which explains the reason & should display,
+ // OR it is hidden but the course has a setting to display hidden sections as unavilable.
$showsection = $thissection->uservisible ||
- ($thissection->visible && !$thissection->available &&
- !empty($thissection->availableinfo));
+ ($thissection->visible && !$thissection->available && !empty($thissection->availableinfo)) ||
+ (!$thissection->visible && !$course->hiddensections);
if (!$showsection) {
- // If the hiddensections option is set to 'show hidden sections in collapsed
- // form', then display the hidden section message - UNLESS the section is
- // hidden by the availability system, which is set to hide the reason.
- if (!$course->hiddensections && $thissection->available) {
- echo $this->section_hidden($section, $course->id);
- }
-
continue;
}
upgrade_plugin_savepoint(true, 2018030900, 'format', 'topics');
}
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
upgrade_plugin_savepoint(true, 2018030900, 'format', 'weeks');
}
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
if ($displaycourselisting) {
echo $renderer->grid_column_start($coursesize, 'course-listing');
if (!$issearching) {
- echo $renderer->course_listing($category, $course, $page, $perpage);
+ echo $renderer->course_listing($category, $course, $page, $perpage, $viewmode);
} else {
list($courses, $coursescount, $coursestotal) =
\core_course\management\helper::search_courses($search, $blocklist, $modulelist, $page, $perpage);
return $hubdescription;
}
+
+ /**
+ * Output frontpage summary text and frontpage modules (stored as section 1 in site course)
+ *
+ * This may be disabled in settings
+ *
+ * @return string
+ */
+ public function frontpage_section1() {
+ global $SITE, $USER;
+
+ $output = '';
+ $editing = $this->page->user_is_editing();
+
+ if ($editing) {
+ // Make sure section with number 1 exists.
+ course_create_sections_if_missing($SITE, 1);
+ }
+
+ $modinfo = get_fast_modinfo($SITE);
+ $section = $modinfo->get_section_info(1);
+ if (($section && (!empty($modinfo->sections[1]) or !empty($section->summary))) or $editing) {
+ $output .= $this->box_start('generalbox sitetopic');
+
+ // If currently moving a file then show the current clipboard.
+ if (ismoving($SITE->id)) {
+ $stractivityclipboard = strip_tags(get_string('activityclipboard', '', $USER->activitycopyname));
+ $output .= '<p><font size="2">';
+ $cancelcopyurl = new moodle_url('/course/mod.php', ['cancelcopy' => 'true', 'sesskey' => sesskey()]);
+ $output .= "$stractivityclipboard (" . html_writer::link($cancelcopyurl, get_string('cancel')) .')';
+ $output .= '</font></p>';
+ }
+
+ $context = context_course::instance(SITEID);
+
+ // If the section name is set we show it.
+ if (trim($section->name) !== '') {
+ $output .= $this->heading(
+ format_string($section->name, true, array('context' => $context)),
+ 2,
+ 'sectionname'
+ );
+ }
+
+ $summarytext = file_rewrite_pluginfile_urls($section->summary,
+ 'pluginfile.php',
+ $context->id,
+ 'course',
+ 'section',
+ $section->id);
+ $summaryformatoptions = new stdClass();
+ $summaryformatoptions->noclean = true;
+ $summaryformatoptions->overflowdiv = true;
+
+ $output .= format_text($summarytext, $section->summaryformat, $summaryformatoptions);
+
+ if ($editing && has_capability('moodle/course:update', $context)) {
+ $streditsummary = get_string('editsummary');
+ $editsectionurl = new moodle_url('/course/editsection.php', ['id' => $section->id]);
+ $output .= html_writer::link($editsectionurl, $this->pix_icon('t/edit', $streditsummary)) .
+ "<br /><br />";
+ }
+
+ $output .= $this->course_section_cm_list($SITE, $section);
+
+ $output .= $this->course_section_add_cm_control($SITE, $section->section);
+ $output .= $this->box_end();
+ }
+
+ return $output;
+ }
+
+ /**
+ * Output news for the frontpage (extract from site-wide news forum)
+ *
+ * @param stdClass $newsforum record from db table 'forum' that represents the site news forum
+ * @return string
+ */
+ protected function frontpage_news($newsforum) {
+ global $CFG, $SITE, $SESSION, $USER;
+ require_once($CFG->dirroot .'/mod/forum/lib.php');
+
+ $output = '';
+
+ if (isloggedin()) {
+ $SESSION->fromdiscussion = $CFG->wwwroot;
+ $subtext = '';
+ if (\mod_forum\subscriptions::is_subscribed($USER->id, $newsforum)) {
+ if (!\mod_forum\subscriptions::is_forcesubscribed($newsforum)) {
+ $subtext = get_string('unsubscribe', 'forum');
+ }
+ } else {
+ $subtext = get_string('subscribe', 'forum');
+ }
+ $suburl = new moodle_url('/mod/forum/subscribe.php', array('id' => $newsforum->id, 'sesskey' => sesskey()));
+ $output .= html_writer::tag('div', html_writer::link($suburl, $subtext), array('class' => 'subscribelink'));
+ }
+
+ ob_start();
+ forum_print_latest_discussions($SITE, $newsforum, $SITE->newsitems, 'plain', 'p.modified DESC');
+ $output .= ob_get_contents();
+ ob_end_clean();
+
+ return $output;
+ }
+
+ /**
+ * Renders part of frontpage with a skip link (i.e. "My courses", "Site news", etc.)
+ *
+ * @param string $skipdivid
+ * @param string $contentsdivid
+ * @param string $header Header of the part
+ * @param string $contents Contents of the part
+ * @return string
+ */
+ protected function frontpage_part($skipdivid, $contentsdivid, $header, $contents) {
+ $output = html_writer::link('#' . $skipdivid,
+ get_string('skipa', 'access', core_text::strtolower(strip_tags($header))),
+ array('class' => 'skip-block skip'));
+
+ // Wrap frontpage part in div container.
+ $output .= html_writer::start_tag('div', array('id' => $contentsdivid));
+ $output .= $this->heading($header);
+
+ $output .= $contents;
+
+ // End frontpage part div container.
+ $output .= html_writer::end_tag('div');
+
+ $output .= html_writer::tag('span', '', array('class' => 'skip-block-to', 'id' => $skipdivid));
+ return $output;
+ }
+
+ /**
+ * Outputs contents for frontpage as configured in $CFG->frontpage or $CFG->frontpageloggedin
+ *
+ * @return string
+ */
+ public function frontpage() {
+ global $CFG, $SITE;
+
+ $output = '';
+
+ if (isloggedin() and !isguestuser() and isset($CFG->frontpageloggedin)) {
+ $frontpagelayout = $CFG->frontpageloggedin;
+ } else {
+ $frontpagelayout = $CFG->frontpage;
+ }
+
+ foreach (explode(',', $frontpagelayout) as $v) {
+ switch ($v) {
+ // Display the main part of the front page.
+ case FRONTPAGENEWS:
+ if ($SITE->newsitems) {
+ // Print forums only when needed.
+ require_once($CFG->dirroot .'/mod/forum/lib.php');
+ if (($newsforum = forum_get_course_forum($SITE->id, 'news')) &&
+ ($forumcontents = $this->frontpage_news($newsforum))) {
+ $newsforumcm = get_fast_modinfo($SITE)->instances['forum'][$newsforum->id];
+ $output .= $this->frontpage_part('skipsitenews', 'site-news-forum',
+ $newsforumcm->get_formatted_name(), $forumcontents);
+ }
+ }
+ break;
+
+ case FRONTPAGEENROLLEDCOURSELIST:
+ $mycourseshtml = $this->frontpage_my_courses();
+ if (!empty($mycourseshtml)) {
+ $output .= $this->frontpage_part('skipmycourses', 'frontpage-course-list',
+ get_string('mycourses'), $mycourseshtml);
+ break;
+ }
+ // No "break" here. If there are no enrolled courses - continue to 'Available courses'.
+
+ case FRONTPAGEALLCOURSELIST:
+ $availablecourseshtml = $this->frontpage_available_courses();
+ if (!empty($availablecourseshtml)) {
+ $output .= $this->frontpage_part('skipavailablecourses', 'frontpage-available-course-list',
+ get_string('availablecourses'), $availablecourseshtml);
+ }
+ break;
+
+ case FRONTPAGECATEGORYNAMES:
+ $output .= $this->frontpage_part('skipcategories', 'frontpage-category-names',
+ get_string('categories'), $this->frontpage_categories_list());
+ break;
+
+ case FRONTPAGECATEGORYCOMBO:
+ $output .= $this->frontpage_part('skipcourses', 'frontpage-category-combo',
+ get_string('courses'), $this->frontpage_combo_list());
+ break;
+
+ case FRONTPAGECOURSESEARCH:
+ $output .= $this->box($this->course_search_form('', 'short'), 'mdl-align');
+ break;
+
+ }
+ $output .= '<br />';
+ }
+
+ return $output;
+ }
}
/**
// Test move the marked section down..
move_section_to($course, 2, 4);
- // Verify that the coruse marker has been moved along with the section..
+ // Verify that the course marker has been moved along with the section..
$course = $DB->get_record('course', array('id' => $course->id));
$this->assertEquals(4, $course->marker);
public static function delete_data_for_all_users_in_context(\context $context) {
global $DB;
- if (empty($context)) {
- return;
- }
// Sanity check that context is at the User context level.
if ($context->contextlevel == CONTEXT_COURSE) {
$sql = "SELECT ue.id
$DB->delete_records_select('user_enrolments', "id $sql", $params);
}
+ /**
+ * Get the subcontext for export.
+ *
+ * @param array $subcontext Any additional subcontext to use.
+ * @return array The array containing the full subcontext, i.e. [enrolments, subcontext]
+ */
+ public static function get_subcontext(array $subcontext) {
+ return array_merge(
+ [get_string('privacy:metadata:user_enrolments', 'core_enrol')],
+ $subcontext
+ );
+ }
}
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace enrol_flatfile\privacy;
+use core_privacy\local\metadata\collection;
+use core_privacy\local\request\approved_contextlist;
+use core_privacy\local\request\context;
+use core_privacy\local\request\contextlist;
+use core_privacy\local\request\writer;
+use core_privacy\local\request\transform;
+
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for enrol_flatfile implementing null_provider.
* @copyright 2018 Carlos Escobedo <carlos@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-class provider implements \core_privacy\local\metadata\null_provider {
+class provider implements
+ \core_privacy\local\metadata\provider,
+ \core_privacy\local\request\plugin\provider {
+
/**
- * Get the language string identifier with the component's language
- * file to explain why this plugin stores no data.
+ * Returns meta data about this system.
*
- * @return string
+ * @param collection $collection The initialised collection to add items to.
+ * @return collection A listing of user data stored through this system.
*/
- public static function get_reason() : string {
- return 'privacy:metadata';
+ public static function get_metadata(collection $collection) : collection {
+ return $collection->add_database_table('enrol_flatfile', [
+ 'action' => 'privacy:metadata:enrol_flatfile:action',
+ 'roleid' => 'privacy:metadata:enrol_flatfile:roleid',
+ 'userid' => 'privacy:metadata:enrol_flatfile:userid',
+ 'courseid' => 'privacy:metadata:enrol_flatfile:courseid',
+ 'timestart' => 'privacy:metadata:enrol_flatfile:timestart',
+ 'timeend' => 'privacy:metadata:enrol_flatfile:timeend',
+ 'timemodified' => 'privacy:metadata:enrol_flatfile:timemodified'
+ ], 'privacy:metadata:enrol_flatfile');
}
-}
\ No newline at end of file
+
+ /**
+ * Get the list of contexts that contain user information for the specified user.
+ *
+ * @param int $userid The user to search.
+ * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
+ */
+ public static function get_contexts_for_userid(int $userid) : contextlist {
+ $sql = "SELECT c.id
+ FROM {enrol_flatfile} ef
+ JOIN {context} c ON c.contextlevel = ? AND c.instanceid = ef.courseid
+ WHERE ef.userid = ?";
+ $params = [CONTEXT_COURSE, $userid];
+
+ $contextlist = new contextlist();
+ $contextlist->set_component('enrol_flatfile');
+ return $contextlist->add_from_sql($sql, $params);
+ }
+
+ /**
+ * Export all user data for the specified user, in the specified contexts.
+ *
+ * @param approved_contextlist $contextlist The approved contexts to export information for.
+ */
+ public static function export_user_data(approved_contextlist $contextlist) {
+ global $DB;
+
+ // Ensure all contexts are CONTEXT_COURSE.
+ $contexts = static::validate_contextlist_contexts($contextlist);
+ if (empty($contexts)) {
+ return;
+ }
+
+ // Get the context instance ids from the contexts. These are the course ids..
+ $contextinstanceids = array_map(function($context) {
+ return $context->instanceid;
+ }, $contexts);
+ $userid = $contextlist->get_user()->id;
+
+ // Now, we just need to fetch and format all entries corresponding to the contextids provided.
+ $sql = "SELECT ef.action, r.shortname, ef.courseid, ef.timestart, ef.timeend, ef.timemodified
+ FROM {enrol_flatfile} ef
+ JOIN {context} c ON c.contextlevel = :contextlevel AND c.instanceid = ef.courseid
+ JOIN {role} r ON r.id = ef.roleid
+ WHERE ef.userid = :userid";
+ $params = ['contextlevel' => CONTEXT_COURSE, 'userid' => $userid];
+ list($insql, $inparams) = $DB->get_in_or_equal($contextinstanceids, SQL_PARAMS_NAMED);
+ $sql .= " AND ef.courseid $insql";
+ $params = array_merge($params, $inparams);
+
+ $futureenrolments = $DB->get_recordset_sql($sql, $params);
+ $enrolmentdata = [];
+ foreach ($futureenrolments as $futureenrolment) {
+ // It's possible to have more than one future enrolment per course.
+ $futureenrolment->timestart = transform::datetime($futureenrolment->timestart);
+ $futureenrolment->timeend = transform::datetime($futureenrolment->timeend);
+ $futureenrolment->timemodified = transform::datetime($futureenrolment->timemodified);
+ $enrolmentdata[$futureenrolment->courseid][] = $futureenrolment;
+ }
+ $futureenrolments->close();
+
+ // And finally, write out the data to the relevant course contexts.
+ $subcontext = \core_enrol\privacy\provider::get_subcontext([get_string('pluginname', 'enrol_flatfile')]);
+ foreach ($enrolmentdata as $courseid => $enrolments) {
+ $data = (object) [
+ 'pendingenrolments' => $enrolments,
+ ];
+ writer::with_context(\context_course::instance($courseid))->export_data($subcontext, $data);
+ }
+ }
+
+ /**
+ * Delete all data for all users in the specified context.
+ *
+ * @param \context $context The specific context to delete data for.
+ */
+ public static function delete_data_for_all_users_in_context(\context $context) {
+ if ($context->contextlevel != CONTEXT_COURSE) {
+ return;
+ }
+ global $DB;
+ $DB->delete_records('enrol_flatfile', ['courseid' => $context->instanceid]);
+ }
+
+ /**
+ * Delete all user data for the specified user, in the specified contexts.
+ *
+ * @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
+ */
+ public static function delete_data_for_user(approved_contextlist $contextlist) {
+ // Only delete data from contexts which are at the COURSE_MODULE contextlevel.
+ $contexts = self::validate_contextlist_contexts($contextlist);
+ if (empty($contexts)) {
+ return;
+ }
+
+ // Get the course ids based on the provided contexts.
+ $contextinstanceids = array_map(function($context) {
+ return $context->instanceid;
+ }, $contextlist->get_contexts());
+
+ global $DB;
+ $user = $contextlist->get_user();
+ list($insql, $inparams) = $DB->get_in_or_equal($contextinstanceids, SQL_PARAMS_NAMED);
+ $params = array_merge(['userid' => $user->id], $inparams);
+ $sql = "userid = :userid AND courseid $insql";
+ $DB->delete_records_select('enrol_flatfile', $sql, $params);
+ }
+
+ /**
+ * Simple sanity check on the contextlist contexts, making sure they're of CONTEXT_COURSE contextlevel.
+ *
+ * @param approved_contextlist $contextlist
+ * @return array the array of contexts filtered to only include those of CONTEXT_COURSE contextlevel.
+ */
+ protected static function validate_contextlist_contexts(approved_contextlist $contextlist) {
+ return array_reduce($contextlist->get_contexts(), function($carry, $context) {
+ if ($context->contextlevel == CONTEXT_COURSE) {
+ $carry[] = $context;
+ }
+ return $carry;
+ }, []);
+ }
+}
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
$string['filelockedmailsubject'] = 'Important error: Enrolment file';
$string['flatfile:manage'] = 'Manage user enrolments manually';
$string['flatfile:unenrol'] = 'Unenrol users from the course manually';
+$string['flatfileenrolments'] = 'Flat file (CSV) enrolments';
$string['flatfilesync'] = 'Flat file enrolment sync';
$string['location'] = 'File location';
$string['location_desc'] = 'Specify full path to the enrolment file. The file is automatically deleted after processing.';
del, student, 17, CF101
add, student, 21, CF101, 1091115000, 1091215000
</pre>';
-$string['privacy:metadata'] = 'The Flat file (CSV) enrolment plugin does not store any personal data.';
+$string['privacy:metadata:enrol_flatfile'] = 'The Flat file (CSV) enrolment plugin may store personal data relating to future enrolments in the enrol_flatfile table.';
+$string['privacy:metadata:enrol_flatfile:action'] = 'The enrolment action expected at the given date.';
+$string['privacy:metadata:enrol_flatfile:courseid'] = 'The courseid to which the enrolment relates.';
+$string['privacy:metadata:enrol_flatfile:roleid'] = 'The id of the role to be assigned or revoked.';
+$string['privacy:metadata:enrol_flatfile:timestart'] = 'The time at which the enrolment change starts.';
+$string['privacy:metadata:enrol_flatfile:timeend'] = 'The time at which the enrolment change ends.';
+$string['privacy:metadata:enrol_flatfile:timemodified'] = 'The modification time of this enrolment change.';
+$string['privacy:metadata:enrol_flatfile:userid'] = 'The id of the user to which the role assignment relates.';
--- /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/>.
+
+/**
+ * Privacy tests for enrol_flatfile.
+ *
+ * @package enrol_flatfile
+ * @category test
+ * @copyright 2018 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+use core_privacy\local\metadata\collection;
+use core_privacy\tests\provider_testcase;
+use core_privacy\local\request\approved_contextlist;
+use core_privacy\local\request\writer;
+use enrol_flatfile\privacy\provider;
+
+/**
+ * Privacy tests for enrol_flatfile.
+ *
+ * @copyright 2018 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class enrol_flatfile_privacy_testcase extends provider_testcase {
+
+ /** @var \stdClass $user1 a test user.*/
+ protected $user1;
+
+ /** @var \stdClass $user2 a test user.*/
+ protected $user2;
+
+ /** @var \context $coursecontext1 a course context.*/
+ protected $coursecontext1;
+
+ /** @var \context $coursecontext2 a course context.*/
+ protected $coursecontext2;
+
+ /** @var \context $coursecontext3 a course context.*/
+ protected $coursecontext3;
+
+ /**
+ * Called before every test.
+ */
+ public function setUp() {
+ $this->resetAfterTest(true);
+ }
+
+ /**
+ * Verify that get_metadata returns the database table mapping.
+ */
+ public function test_get_metadata() {
+ $collection = new collection('enrol_flatfile');
+ $collection = provider::get_metadata($collection);
+ $collectiondata = $collection->get_collection();
+ $this->assertNotEmpty($collectiondata);
+ $this->assertInstanceOf(\core_privacy\local\metadata\types\database_table::class, $collectiondata[0]);
+ }
+
+ /**
+ * Verify that the relevant course contexts are returned for users with pending enrolment records.
+ */
+ public function test_get_contexts_for_user() {
+ global $DB;
+ // Create, via flatfile syncing, the future enrolments entries in the enrol_flatfile table.
+ $this->create_future_enrolments();
+
+ $this->assertEquals(3, $DB->count_records('enrol_flatfile'));
+
+ // We expect to see 2 entries for user1, in course1 and course3.
+ $contextlist = provider::get_contexts_for_userid($this->user1->id);
+ $this->assertEquals(2, $contextlist->count());
+ $contextids = $contextlist->get_contextids();
+ $this->assertContains($this->coursecontext1->id, $contextids);
+ $this->assertContains($this->coursecontext3->id, $contextids);
+
+ // And 1 for user2 on course2.
+ $contextlist = provider::get_contexts_for_userid($this->user2->id);
+ $this->assertEquals(1, $contextlist->count());
+ $contextids = $contextlist->get_contextids();
+ $this->assertContains($this->coursecontext2->id, $contextids);
+ }
+
+ /**
+ * Verify the export includes any future enrolment records for the user.
+ */
+ public function test_export_user_data() {
+ // Create, via flatfile syncing, the future enrolments entries in the enrol_flatfile table.
+ $this->create_future_enrolments();
+
+ // Get contexts containing user data.
+ $contextlist = provider::get_contexts_for_userid($this->user1->id);
+ $this->assertEquals(2, $contextlist->count());
+
+ $approvedcontextlist = new approved_contextlist(
+ $this->user1,
+ 'enrol_flatfile',
+ $contextlist->get_contextids()
+ );
+
+ // Export for the approved contexts.
+ provider::export_user_data($approvedcontextlist);
+
+ // Verify we see one future course enrolment in course1, and one in course3.
+ $subcontext = \core_enrol\privacy\provider::get_subcontext([get_string('pluginname', 'enrol_flatfile')]);
+
+ $writer = writer::with_context($this->coursecontext1);
+ $this->assertNotEmpty($writer->get_data($subcontext));
+
+ $writer = writer::with_context($this->coursecontext3);
+ $this->assertNotEmpty($writer->get_data($subcontext));
+
+ // Verify we have nothing in course 2 for this user.
+ $writer = writer::with_context($this->coursecontext2);
+ $this->assertEmpty($writer->get_data($subcontext));
+ }
+
+ /**
+ * Verify export will limit any future enrolment records to only those contextids provided.
+ */
+ public function test_export_user_data_restricted_context_subset() {
+ // Create, via flatfile syncing, the future enrolments entries in the enrol_flatfile table.
+ $this->create_future_enrolments();
+
+ // Now, limit the export scope to just course1's context and verify only that data is seen in any export.
+ $subsetapprovedcontextlist = new approved_contextlist(
+ $this->user1,
+ 'enrol_flatfile',
+ [$this->coursecontext1->id]
+ );
+
+ // Export for the approved contexts.
+ provider::export_user_data($subsetapprovedcontextlist);
+
+ // Verify we see one future course enrolment in course1 only.
+ $subcontext = \core_enrol\privacy\provider::get_subcontext([get_string('pluginname', 'enrol_flatfile')]);
+
+ $writer = writer::with_context($this->coursecontext1);
+ $this->assertNotEmpty($writer->get_data($subcontext));
+
+ // And nothing in the course3 context.
+ $writer = writer::with_context($this->coursecontext3);
+ $this->assertEmpty($writer->get_data($subcontext));
+ }
+
+ /**
+ * Verify that records can be deleted by context.
+ */
+ public function test_delete_data_for_all_users_in_context() {
+ global $DB;
+ // Create, via flatfile syncing, the future enrolments entries in the enrol_flatfile table.
+ $this->create_future_enrolments();
+
+ // Verify we have 1 future enrolments for course 1.
+ $this->assertEquals(1, $DB->count_records('enrol_flatfile', ['courseid' => $this->coursecontext1->instanceid]));
+
+ // Now, run delete by context and confirm that record is removed.
+ provider::delete_data_for_all_users_in_context($this->coursecontext1);
+ $this->assertEquals(0, $DB->count_records('enrol_flatfile', ['courseid' => $this->coursecontext1->instanceid]));
+ }
+
+ public function test_delete_data_for_user() {
+ global $DB;
+ // Create, via flatfile syncing, the future enrolments entries in the enrol_flatfile table.
+ $this->create_future_enrolments();
+
+ // Verify we have 2 future enrolments for course 1 and course 3.
+ $contextlist = provider::get_contexts_for_userid($this->user1->id);
+ $this->assertEquals(2, $contextlist->count());
+ $contextids = $contextlist->get_contextids();
+ $this->assertContains($this->coursecontext1->id, $contextids);
+ $this->assertContains($this->coursecontext3->id, $contextids);
+
+ $approvedcontextlist = new approved_contextlist(
+ $this->user1,
+ 'enrol_flatfile',
+ $contextids
+ );
+
+ // Now, run delete for user and confirm that both records are removed.
+ provider::delete_data_for_user($approvedcontextlist);
+ $contextlist = provider::get_contexts_for_userid($this->user1->id);
+ $this->assertEquals(0, $contextlist->count());
+ $this->assertEquals(0, $DB->count_records('enrol_flatfile', ['userid' => $this->user1->id]));
+ }
+
+ /**
+ * Helper to sync a file and create the enrol_flatfile DB entries, for use with the get, export and delete tests.
+ */
+ protected function create_future_enrolments() {
+ global $CFG;
+ $this->user1 = $this->getDataGenerator()->create_user(['idnumber' => 'u1']);
+ $this->user2 = $this->getDataGenerator()->create_user(['idnumber' => 'u2']);
+
+ $course1 = $this->getDataGenerator()->create_course(['idnumber' => 'c1']);
+ $course2 = $this->getDataGenerator()->create_course(['idnumber' => 'c2']);
+ $course3 = $this->getDataGenerator()->create_course(['idnumber' => 'c3']);
+ $this->coursecontext1 = context_course::instance($course1->id);
+ $this->coursecontext2 = context_course::instance($course2->id);
+ $this->coursecontext3 = context_course::instance($course3->id);
+
+ $now = time();
+ $future = $now + 60 * 60 * 5;
+ $farfuture = $now + 60 * 60 * 24 * 5;
+
+ $file = "$CFG->dataroot/enrol.txt";
+ $data = "add,student,u1,c1,$future,0
+ add,student,u2,c2,$future,0
+ add,student,u1,c3,$future,$farfuture";
+ file_put_contents($file, $data);
+
+ $trace = new null_progress_trace();
+ $this->enable_plugin();
+ $flatfileplugin = enrol_get_plugin('flatfile');
+ $flatfileplugin->set_config('location', $file);
+ $flatfileplugin->sync($trace);
+ }
+
+ /**
+ * Enables the flatfile plugin for testing.
+ */
+ protected function enable_plugin() {
+ $enabled = enrol_get_plugins(true);
+ $enabled['flatfile'] = true;
+ $enabled = array_keys($enabled);
+ set_config('enrol_plugins_enabled', implode(',', $enabled));
+ }
+}
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
/* Make videos as wide as possible without being wider than their containers */
width: 100vw;
max-width: 100%;
- height: auto;
}
.mediaplugin > div {
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
*/
public static function get_metadata(collection $collection) : collection {
+ // Tables without 'real' user information.
$collection->add_database_table('grade_outcomes', [
'timemodified' => 'privacy:metadata:outcomes:timemodified',
'usermodified' => 'privacy:metadata:outcomes:usermodified',
'loggeduser' => 'privacy:metadata:history:loggeduser',
], 'privacy:metadata:itemshistory');
+ $collection->add_database_table('scale', [
+ 'userid' => 'privacy:metadata:scale:userid',
+ 'timemodified' => 'privacy:metadata:scale:timemodified',
+ ], 'privacy:metadata:scale');
+
+ $collection->add_database_table('scale_history', [
+ 'userid' => 'privacy:metadata:scale:userid',
+ 'timemodified' => 'privacy:metadata:history:timemodified',
+ 'loggeduser' => 'privacy:metadata:history:loggeduser',
+ ], 'privacy:metadata:scalehistory');
+
+ // Table with user information.
$gradescommonfields = [
'userid' => 'privacy:metadata:grades:userid',
'usermodified' => 'privacy:metadata:grades:usermodified',
'loggeduser' => 'privacy:metadata:history:loggeduser',
]), 'privacy:metadata:gradeshistory');
- // The table grade_import_values is not reported because its data is temporary and only
+ // The following tables are reported but not exported/deleted because their data is temporary and only
// used during an import. It's content is deleted after a successful, or failed, import.
+ $collection->add_database_table('grade_import_newitem', [
+ 'itemname' => 'privacy:metadata:grade_import_newitem:itemname',
+ 'importcode' => 'privacy:metadata:grade_import_newitem:importcode',
+ 'importer' => 'privacy:metadata:grade_import_newitem:importer'
+ ], 'privacy:metadata:grade_import_newitem');
+
+ $collection->add_database_table('grade_import_values', [
+ 'userid' => 'privacy:metadata:grade_import_values:userid',
+ 'finalgrade' => 'privacy:metadata:grade_import_values:finalgrade',
+ 'feedback' => 'privacy:metadata:grade_import_values:feedback',
+ 'importcode' => 'privacy:metadata:grade_import_values:importcode',
+ 'importer' => 'privacy:metadata:grade_import_values:importer',
+ 'importonlyfeedback' => 'privacy:metadata:grade_import_values:importonlyfeedback'
+ ], 'privacy:metadata:grade_import_values');
+
return $collection;
}
FROM {grade_outcomes} go
JOIN {context} ctx
ON (go.courseid > 0 AND ctx.instanceid = go.courseid AND ctx.contextlevel = :courselevel)
- OR (ctx.id = :syscontextid)
+ OR ((go.courseid IS NULL OR go.courseid < 1) AND ctx.id = :syscontextid)
WHERE go.usermodified = :userid";
$params = ['userid' => $userid, 'courselevel' => CONTEXT_COURSE, 'syscontextid' => SYSCONTEXTID];
$contextlist->add_from_sql($sql, $params);
- // Add where appear in the history of outcomes, categories or items.
+ // Add where we modified scales.
+ $sql = "
+ SELECT DISTINCT ctx.id
+ FROM {scale} s
+ JOIN {context} ctx
+ ON (s.courseid > 0 AND ctx.instanceid = s.courseid AND ctx.contextlevel = :courselevel)
+ OR (s.courseid = 0 AND ctx.id = :syscontextid)
+ WHERE s.userid = :userid";
+ $params = ['userid' => $userid, 'courselevel' => CONTEXT_COURSE, 'syscontextid' => SYSCONTEXTID];
+ $contextlist->add_from_sql($sql, $params);
+
+ // Add where appear in the history of outcomes, categories, scales or items.
$sql = "
SELECT DISTINCT ctx.id
FROM {context} ctx
LEFT JOIN {grade_outcomes_history} goh ON goh.loggeduser = :userid1 AND (
(goh.courseid > 0 AND goh.courseid = ctx.instanceid AND ctx.contextlevel = :courselevel1)
- OR ((goh.courseid IS NULL OR goh.courseid < 1) AND ctx.id = :syscontextid)
+ OR ((goh.courseid IS NULL OR goh.courseid < 1) AND ctx.id = :syscontextid1)
)
LEFT JOIN {grade_categories_history} gch ON gch.loggeduser = :userid2 AND (
gch.courseid = ctx.instanceid
gih.courseid = ctx.instanceid
AND ctx.contextlevel = :courselevel3
)
+ LEFT JOIN {scale_history} sh
+ ON (sh.userid = :userid4 OR sh.loggeduser = :userid5)
+ AND (
+ (sh.courseid > 0 AND sh.courseid = ctx.instanceid AND ctx.contextlevel = :courselevel4)
+ OR (sh.courseid = 0 AND ctx.id = :syscontextid2)
+ )
WHERE goh.id IS NOT NULL
OR gch.id IS NOT NULL
- OR gih.id IS NOT NULL";
+ OR gih.id IS NOT NULL
+ OR sh.id IS NOT NULL";
$params = [
- 'syscontextid' => SYSCONTEXTID,
+ 'syscontextid1' => SYSCONTEXTID,
+ 'syscontextid2' => SYSCONTEXTID,
'courselevel1' => CONTEXT_COURSE,
'courselevel2' => CONTEXT_COURSE,
'courselevel3' => CONTEXT_COURSE,
+ 'courselevel4' => CONTEXT_COURSE,
'userid1' => $userid,
'userid2' => $userid,
'userid3' => $userid,
+ 'userid4' => $userid,
+ 'userid5' => $userid,
];
$contextlist->add_from_sql($sql, $params);
// Export the outcomes.
static::export_user_data_outcomes_in_contexts($contextlist);
+ // Export the scales.
+ static::export_user_data_scales_in_contexts($contextlist);
+
// Export the historical grades which have become orphans (their grade items were deleted).
// We place those in ther user context of the graded user.
$userids = array_values(array_map(function($context) {
});
}
+ /**
+ * Export the user data related to scales.
+ *
+ * @param approved_contextlist $contextlist The approved contexts to export information for.
+ * @return void
+ */
+ protected static function export_user_data_scales_in_contexts(approved_contextlist $contextlist) {
+ global $DB;
+
+ $rootpath = [get_string('grades', 'core_grades')];
+ $relatedtomepath = array_merge($rootpath, [get_string('privacy:path:relatedtome', 'core_grades')]);
+ $userid = $contextlist->get_user()->id;
+
+ // Reorganise the contexts.
+ $reduced = array_reduce($contextlist->get_contexts(), function($carry, $context) {
+ if ($context->contextlevel == CONTEXT_SYSTEM) {
+ $carry['in_system'] = true;
+ } else if ($context->contextlevel == CONTEXT_COURSE) {
+ $carry['courseids'][] = $context->instanceid;
+ }
+ return $carry;
+ }, [
+ 'in_system' => false,
+ 'courseids' => []
+ ]);
+
+ // Construct SQL.
+ $sqltemplateparts = [];
+ $templateparams = [];
+ if ($reduced['in_system']) {
+ $sqltemplateparts[] = '{prefix}.courseid = 0';
+ }
+ if (!empty($reduced['courseids'])) {
+ list($insql, $inparams) = $DB->get_in_or_equal($reduced['courseids'], SQL_PARAMS_NAMED);
+ $sqltemplateparts[] = "{prefix}.courseid $insql";
+ $templateparams = array_merge($templateparams, $inparams);
+ }
+ if (empty($sqltemplateparts)) {
+ return;
+ }
+ $sqltemplate = '(' . implode(' OR ', $sqltemplateparts) . ')';
+
+ // Export edited scales.
+ $sqlwhere = str_replace('{prefix}', 's', $sqltemplate);
+ $sql = "
+ SELECT s.id, s.courseid, s.name, s.timemodified
+ FROM {scale} s
+ WHERE $sqlwhere
+ AND s.userid = :userid
+ ORDER BY s.courseid, s.timemodified, s.id";
+ $params = array_merge($templateparams, ['userid' => $userid]);
+ $recordset = $DB->get_recordset_sql($sql, $params);
+ static::recordset_loop_and_export($recordset, 'courseid', [], function($carry, $record) {
+ $carry[] = [
+ 'name' => $record->name,
+ 'timemodified' => transform::datetime($record->timemodified),
+ 'created_or_modified_by_you' => transform::yesno(true)
+ ];
+ return $carry;
+
+ }, function($courseid, $data) use ($relatedtomepath) {
+ $context = $courseid ? context_course::instance($courseid) : context_system::instance();
+ writer::with_context($context)->export_related_data($relatedtomepath, 'scales',
+ (object) ['scales' => $data]);
+ });
+
+ // Export edits of scales history.
+ $sqlwhere = str_replace('{prefix}', 'sh', $sqltemplate);
+ $sql = "
+ SELECT sh.id, sh.courseid, sh.name, sh.userid, sh.timemodified, sh.action, sh.loggeduser
+ FROM {scale_history} sh
+ WHERE $sqlwhere
+ AND sh.loggeduser = :userid1
+ OR sh.userid = :userid2
+ ORDER BY sh.courseid, sh.timemodified, sh.id";
+ $params = array_merge($templateparams, ['userid1' => $userid, 'userid2' => $userid]);
+ $recordset = $DB->get_recordset_sql($sql, $params);
+ static::recordset_loop_and_export($recordset, 'courseid', [], function($carry, $record) use ($userid) {
+ $carry[] = [
+ 'name' => $record->name,
+ 'timemodified' => transform::datetime($record->timemodified),
+ 'author_of_change_was_you' => transform::yesno($record->userid == $userid),
+ 'author_of_action_was_you' => transform::yesno($record->loggeduser == $userid),
+ 'action' => static::transform_history_action($record->action)
+ ];
+ return $carry;
+
+ }, function($courseid, $data) use ($relatedtomepath) {
+ $context = $courseid ? context_course::instance($courseid) : context_system::instance();
+ writer::with_context($context)->export_related_data($relatedtomepath, 'scales_history',
+ (object) ['modified_records' => $data]);
+ });
+ }
+
/**
* Extract grade_grade from a record.
*
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
// Automatically generated Moodle v3.4.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v3.5.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
$u4 = $dg->create_user();
$u5 = $dg->create_user();
$u6 = $dg->create_user();
+ $u7 = $dg->create_user();
+ $u8 = $dg->create_user();
+ $u9 = $dg->create_user();
+ $u10 = $dg->create_user();
+ $u11 = $dg->create_user();
$sysctx = context_system::instance();
$c1ctx = context_course::instance($c1->id);
'fullname' => 'go2']), false);
// Nothing as of now.
- foreach ([$u1, $u2, $u3, $u4] as $u) {
+ foreach ([$u1, $u2, $u3, $u4, $u5, $u6, $u7, $u8, $u9, $u10, $u11] as $u) {
$contexts = array_flip(provider::get_contexts_for_userid($u->id)->get_contextids());
$this->assertEmpty($contexts);
}
$go0 = new grade_outcome(['shortname' => 'go0', 'fullname' => 'go0', 'usermodified' => $u1->id]);
$go0->insert();
- $go1 = new grade_outcome(['shortname' => 'go1', 'fullname' => 'go1', 'courseid' => $c1->id, 'usermodified' => $u1->id]);
+ $go1 = new grade_outcome(['shortname' => 'go1', 'fullname' => 'go1', 'courseid' => $c1->id, 'usermodified' => $u11->id]);
$go1->insert();
+ // Create scales.
+ $s1 = new grade_scale(['name' => 's1', 'scale' => 'a,b', 'userid' => $u7->id, 'courseid' => 0, 'description' => '']);
+ $s1->insert();
+ $s2 = new grade_scale(['name' => 's2', 'scale' => 'a,b', 'userid' => $u8->id, 'courseid' => $c1->id, 'description' => '']);
+ $s2->insert();
+
// User 2 creates history.
$this->setUser($u2);
$go0->shortname .= ' edited';
$this->setUser($u6);
$gi2a->delete();
+ // User 9 creates history.
+ $this->setUser($u9);
+ $s1->name .= ' edited';
+ $s1->update();
+
// Assert contexts.
$contexts = array_flip(provider::get_contexts_for_userid($u1->id)->get_contextids());
- $this->assertCount(2, $contexts);
- $this->assertArrayHasKey($c1ctx->id, $contexts);
+ $this->assertCount(1, $contexts);
$this->assertArrayHasKey($sysctx->id, $contexts);
+ $contexts = array_flip(provider::get_contexts_for_userid($u11->id)->get_contextids());
+ $this->assertCount(1, $contexts);
+ $this->assertArrayHasKey($c1ctx->id, $contexts);
$contexts = array_flip(provider::get_contexts_for_userid($u2->id)->get_contextids());
$this->assertCount(2, $contexts);
$this->assertArrayHasKey($sysctx->id, $contexts);
$contexts = array_flip(provider::get_contexts_for_userid($u6->id)->get_contextids());
$this->assertCount(1, $contexts);
$this->assertArrayHasKey($c2ctx->id, $contexts);
+ $contexts = array_flip(provider::get_contexts_for_userid($u7->id)->get_contextids());
+ $this->assertCount(1, $contexts);
+ $this->assertArrayHasKey($sysctx->id, $contexts);
+ $contexts = array_flip(provider::get_contexts_for_userid($u8->id)->get_contextids());
+ $this->assertCount(1, $contexts);
+ $this->assertArrayHasKey($c1ctx->id, $contexts);
+ $contexts = array_flip(provider::get_contexts_for_userid($u9->id)->get_contextids());
+ $this->assertCount(1, $contexts);
+ $this->assertArrayHasKey($sysctx->id, $contexts);
+
+ // User 10 creates history.
+ $this->setUser($u10);
+ $s2->delete();
+
+ $contexts = array_flip(provider::get_contexts_for_userid($u10->id)->get_contextids());
+ $this->assertCount(1, $contexts);
+ $this->assertArrayHasKey($c1ctx->id, $contexts);
}
public function test_get_contexts_for_userid_grades_and_history() {
$u4 = $dg->create_user();
$u5 = $dg->create_user();
$u6 = $dg->create_user();
+ $u7 = $dg->create_user();
+ $u8 = $dg->create_user();
+ $u9 = $dg->create_user();
+ $u10 = $dg->create_user();
$sysctx = context_system::instance();
$u1ctx = context_user::instance($u1->id);
$go1 = new grade_outcome(['shortname' => 'go1', 'fullname' => 'go1', 'courseid' => $c1->id, 'usermodified' => $u1->id]);
$go1->insert();
+ // Create scales.
+ $s1 = new grade_scale(['name' => 's1', 'scale' => 'a,b', 'userid' => $u7->id, 'courseid' => 0, 'description' => '']);
+ $s1->insert();
+ $s2 = new grade_scale(['name' => 's2', 'scale' => 'a,b', 'userid' => $u8->id, 'courseid' => $c1->id, 'description' => '']);
+ $s2->insert();
+ $s3 = new grade_scale(['name' => 's3', 'scale' => 'a,b', 'userid' => $u8->id, 'courseid' => $c2->id, 'description' => '']);
+ $s3->insert();
+
// User 2 creates history.
$this->setUser($u2);
$go0->shortname .= ' edited';
$this->setUser($u6);
$gi2a->delete();
+ // User 9 creates history.
+ $this->setUser($u9);
+ $s1->name .= ' edited';
+ $s1->update();
+
+ // User 10 creates history.
+ $this->setUser($u10);
+ $s3->delete();
+
$this->setAdminUser();
// Export data for u1.
$this->assertEquals(transform::yesno(true), $data->modified_records[0]['logged_in_user_was_you']);
$this->assertEquals(get_string('privacy:request:historyactiondelete', 'core_grades'),
$data->modified_records[0]['action']);
+
+ // Export data for u7.
+ writer::reset();
+ provider::export_user_data(new approved_contextlist($u7, 'core_grades', $allcontexts));
+ $data = writer::with_context($c1ctx)->get_related_data($relatedtomepath, 'scales');
+ $this->assertEmpty($data);
+ $data = writer::with_context($sysctx)->get_related_data($relatedtomepath, 'scales');
+ $this->assertCount(1, $data->scales);
+ $this->assertEquals($s1->name, $data->scales[0]['name']);
+ $this->assertEquals(transform::yesno(true), $data->scales[0]['created_or_modified_by_you']);
+
+ // Export data for u8.
+ writer::reset();
+ provider::export_user_data(new approved_contextlist($u8, 'core_grades', $allcontexts));
+ $data = writer::with_context($sysctx)->get_related_data($relatedtomepath, 'scales');
+ $this->assertEmpty($data);
+ $data = writer::with_context($c1ctx)->get_related_data($relatedtomepath, 'scales');
+ $this->assertCount(1, $data->scales);
+ $this->assertEquals($s2->name, $data->scales[0]['name']);
+ $this->assertEquals(transform::yesno(true), $data->scales[0]['created_or_modified_by_you']);
+ $data = writer::with_context($c2ctx)->get_related_data($relatedtomepath, 'scales_history');
+ $this->assertCount(2, $data->modified_records);
+ $this->assertEquals($s3->name, $data->modified_records[0]['name']);
+ $this->assertEquals(transform::yesno(true), $data->modified_records[0]['author_of_change_was_you']);
+ $this->assertEquals(transform::yesno(false), $data->modified_records[0]['author_of_action_was_you']);
+ $this->assertEquals(get_string('privacy:request:historyactioninsert', 'core_grades'),
+ $data->modified_records[0]['action']);
+ $this->assertEquals($s3->name, $data->modified_records[1]['name']);
+ $this->assertEquals(transform::yesno(true), $data->modified_records[1]['author_of_change_was_you']);
+ $this->assertEquals(transform::yesno(false), $data->modified_records[1]['author_of_action_was_you']);
+ $this->assertEquals(get_string('privacy:request:historyactiondelete', 'core_grades'),
+ $data->modified_records[1]['action']);
+
+ // Export data for u9.
+ writer::reset();
+ provider::export_user_data(new approved_contextlist($u9, 'core_grades', $allcontexts));
+ $data = writer::with_context($c1ctx)->get_related_data($relatedtomepath, 'scales');
+ $this->assertEmpty($data);
+ $data = writer::with_context($sysctx)->get_related_data($relatedtomepath, 'scales');
+ $this->assertEmpty($data);
+ $data = writer::with_context($sysctx)->get_related_data($relatedtomepath, 'scales_history');
+ $this->assertCount(1, $data->modified_records);
+ $this->assertEquals($s1->name, $data->modified_records[0]['name']);
+ $this->assertEquals(transform::yesno(false), $data->modified_records[0]['author_of_change_was_you']);
+ $this->assertEquals(transform::yesno(true), $data->modified_records[0]['author_of_action_was_you']);
+ $this->assertEquals(get_string('privacy:request:historyactionupdate', 'core_grades'),
+ $data->modified_records[0]['action']);
+
+ // Export data for u10.
+ writer::reset();
+ provider::export_user_data(new approved_contextlist($u10, 'core_grades', $allcontexts));
+ $data = writer::with_context($c1ctx)->get_related_data($relatedtomepath, 'scales');
+ $this->assertEmpty($data);
+ $data = writer::with_context($c2ctx)->get_related_data($relatedtomepath, 'scales');
+ $this->assertEmpty($data);
+ $data = writer::with_context($sysctx)->get_related_data($relatedtomepath, 'scales');
+ $this->assertEmpty($data);
+ $data = writer::with_context($c1ctx)->get_related_data($relatedtomepath, 'scales_history');
+ $this->assertEmpty($data);
+ $data = writer::with_context($sysctx)->get_related_data($relatedtomepath, 'scales_history');
+ $this->assertEmpty($data);
+ $data = writer::with_context($c2ctx)->get_related_data($relatedtomepath, 'scales_history');
+ $this->assertCount(1, $data->modified_records);
+ $this->assertEquals($s3->name, $data->modified_records[0]['name']);
+ $this->assertEquals(transform::yesno(false), $data->modified_records[0]['author_of_change_was_you']);
+ $this->assertEquals(transform::yesno(true), $data->modified_records[0]['author_of_action_was_you']);
+ $this->assertEquals(get_string('privacy:request:historyactiondelete', 'core_grades'),
+ $data->modified_records[0]['action']);
}
/**
$courserenderer = $PAGE->get_renderer('core', 'course');
echo $OUTPUT->header();
-// Print Section or custom info.
$siteformatoptions = course_get_format($SITE)->get_format_options();
$modinfo = get_fast_modinfo($SITE);
-$modnames = get_module_types_names();
-$modnamesplural = get_module_types_names(true);
$modnamesused = $modinfo->get_used_module_names();
-$mods = $modinfo->get_cms();
+// Print Section or custom info.
if (!empty($CFG->customfrontpageinclude)) {
+ // Pre-fill some variables that custom front page might use.
+ $modnames = get_module_types_names();
+ $modnamesplural = get_module_types_names(true);
+ $mods = $modinfo->get_cms();
+
include($CFG->customfrontpageinclude);
} else if ($siteformatoptions['numsections'] > 0) {
- if ($editing) {
- // Make sure section with number 1 exists.
- course_create_sections_if_missing($SITE, 1);
- // Re-request modinfo in case section was created.
- $modinfo = get_fast_modinfo($SITE);
- }
- $section = $modinfo->get_section_info(1);
- if (($section && (!empty($modinfo->sections[1]) or !empty($section->summary))) or $editing) {
- echo $OUTPUT->box_start('generalbox sitetopic');
-
- // If currently moving a file then show the current clipboard.
- if (ismoving($SITE->id)) {
- $stractivityclipboard = strip_tags(get_string('activityclipboard', '', $USER->activitycopyname));
- echo '<p><font size="2">';
- echo "$stractivityclipboard (<a href=\"course/mod.php?cancelcopy=true&sesskey=".sesskey()."\">";
- echo get_string('cancel') . '</a>)';
- echo '</font></p>';
- }
-
- $context = context_course::instance(SITEID);
-
- // If the section name is set we show it.
- if (trim($section->name) !== '') {
- echo $OUTPUT->heading(
- format_string($section->name, true, array('context' => $context)),
- 2,
- 'sectionname'
- );
- }
-
- $summarytext = file_rewrite_pluginfile_urls($section->summary,
- 'pluginfile.php',
- $context->id,
- 'course',
- 'section',
- $section->id);
- $summaryformatoptions = new stdClass();
- $summaryformatoptions->noclean = true;
- $summaryformatoptions->overflowdiv = true;
-
- echo format_text($summarytext, $section->summaryformat, $summaryformatoptions);
-
- if ($editing && has_capability('moodle/course:update', $context)) {
- $streditsummary = get_string('editsummary');
- echo "<a title=\"$streditsummary\" " .
- " href=\"course/editsection.php?id=$section->id\">" . $OUTPUT->pix_icon('t/edit', $streditsummary) .
- "</a><br /><br />";
- }
-
- $courserenderer = $PAGE->get_renderer('core', 'course');
- echo $courserenderer->course_section_cm_list($SITE, $section);
-
- echo $courserenderer->course_section_add_cm_control($SITE, $section->section);
- echo $OUTPUT->box_end();
- }
+ echo $courserenderer->frontpage_section1();
}
// Include course AJAX.
include_course_ajax($SITE, $modnamesused);
-if (isloggedin() and !isguestuser() and isset($CFG->frontpageloggedin)) {
- $frontpagelayout = $CFG->frontpageloggedin;
-} else {
- $frontpagelayout = $CFG->frontpage;
-}
-
-foreach (explode(',', $frontpagelayout) as $v) {
- switch ($v) {
- // Display the main part of the front page.
- case FRONTPAGENEWS:
- if ($SITE->newsitems) {
- // Print forums only when needed.
- require_once($CFG->dirroot .'/mod/forum/lib.php');
-
- if (! $newsforum = forum_get_course_forum($SITE->id, 'news')) {
- print_error('cannotfindorcreateforum', 'forum');
- }
-
- // Fetch news forum context for proper filtering to happen.
- $newsforumcm = get_coursemodule_from_instance('forum', $newsforum->id, $SITE->id, false, MUST_EXIST);
- $newsforumcontext = context_module::instance($newsforumcm->id, MUST_EXIST);
-
- $forumname = format_string($newsforum->name, true, array('context' => $newsforumcontext));
- echo html_writer::link('#skipsitenews',
- get_string('skipa', 'access', core_text::strtolower(strip_tags($forumname))),
- array('class' => 'skip-block skip'));
-
- // Wraps site news forum in div container.
- echo html_writer::start_tag('div', array('id' => 'site-news-forum'));
-
- if (isloggedin()) {
- $SESSION->fromdiscussion = $CFG->wwwroot;
- $subtext = '';
- if (\mod_forum\subscriptions::is_subscribed($USER->id, $newsforum)) {
- if (!\mod_forum\subscriptions::is_forcesubscribed($newsforum)) {
- $subtext = get_string('unsubscribe', 'forum');
- }
- } else {
- $subtext = get_string('subscribe', 'forum');
- }
- echo $OUTPUT->heading($forumname);
- $suburl = new moodle_url('/mod/forum/subscribe.php', array('id' => $newsforum->id, 'sesskey' => sesskey()));
- echo html_writer::tag('div', html_writer::link($suburl, $subtext), array('class' => 'subscribelink'));
- } else {
- echo $OUTPUT->heading($forumname);
- }
-
- forum_print_latest_discussions($SITE, $newsforum, $SITE->newsitems, 'plain', 'p.modified DESC');
-
- // End site news forum div container.
- echo html_writer::end_tag('div');
+echo $courserenderer->frontpage();
- echo html_writer::tag('span', '', array('class' => 'skip-block-to', 'id' => 'skipsitenews'));
- }
- break;
-
- case FRONTPAGEENROLLEDCOURSELIST:
- $mycourseshtml = $courserenderer->frontpage_my_courses();
- if (!empty($mycourseshtml)) {
- echo html_writer::link('#skipmycourses',
- get_string('skipa', 'access', core_text::strtolower(get_string('mycourses'))),
- array('class' => 'skip skip-block'));
-
- // Wrap frontpage course list in div container.
- echo html_writer::start_tag('div', array('id' => 'frontpage-course-list'));
-
- echo $OUTPUT->heading(get_string('mycourses'));
- echo $mycourseshtml;
-
- // End frontpage course list div container.
- echo html_writer::end_tag('div');
-
- echo html_writer::tag('span', '', array('class' => 'skip-block-to', 'id' => 'skipmycourses'));
- break;
- }
- // No "break" here. If there are no enrolled courses - continue to 'Available courses'.
-
- case FRONTPAGEALLCOURSELIST:
- $availablecourseshtml = $courserenderer->frontpage_available_courses();
- if (!empty($availablecourseshtml)) {
- echo html_writer::link('#skipavailablecourses',
- get_string('skipa', 'access', core_text::strtolower(get_string('availablecourses'))),
- array('class' => 'skip skip-block'));
-
- // Wrap frontpage course list in div container.
- echo html_writer::start_tag('div', array('id' => 'frontpage-course-list'));
-
- echo $OUTPUT->heading(get_string('availablecourses'));
- echo $availablecourseshtml;
-
- // End frontpage course list div container.
- echo html_writer::end_tag('div');
-
- echo html_writer::tag('span', '', array('class' => 'skip-block-to', 'id' => 'skipavailablecourses'));
- }
- break;
-
- case FRONTPAGECATEGORYNAMES:
- echo html_writer::link('#skipcategories',
- get_string('skipa', 'access', core_text::strtolower(get_string('categories'))),
- array('class' => 'skip skip-block'));
-
- // Wrap frontpage category names in div container.
- echo html_writer::start_tag('div', array('id' => 'frontpage-category-names'));
-
- echo $OUTPUT->heading(get_string('categories'));
- echo $courserenderer->frontpage_categories_list();
-
- // End frontpage category names div container.
- echo html_writer::end_tag('div');
-
- echo html_writer::tag('span', '', array('class' => 'skip-block-to', 'id' => 'skipcategories'));
- break;
-
- case FRONTPAGECATEGORYCOMBO:
- echo html_writer::link('#skipcourses',
- get_string('skipa', 'access', core_text::strtolower(get_string('courses'))),
- array('class' => 'skip skip-block'));
-
- // Wrap frontpage category combo in div container.
- echo html_writer::start_tag('div', array('id' => 'frontpage-category-combo'));
-
- echo $OUTPUT->heading(get_string('courses'));
- echo $courserenderer->frontpage_combo_list();
-
- // End frontpage category combo div container.
- echo html_writer::end_tag('div');
-
- echo html_writer::tag('span', '', array('class' => 'skip-block-to', 'id' => 'skipcourses'));
- break;
-
- case FRONTPAGECOURSESEARCH:
- echo $OUTPUT->box($courserenderer->course_search_form('', 'short'), 'mdl-align');
- break;
-
- }
- echo '<br />';
-}
if ($editing && has_capability('moodle/course:create', context_system::instance())) {
echo $courserenderer->add_new_course_button();
}
$string['clitypevaluedefault'] = 'iteklado ang halaga, pindutin ang Enter para magamit ang default na halaga ({$a})';
$string['cliunknowoption'] = 'Di-kilalang opsiyon:
{$a}
-Gamit po ang --help na opsiyon';
+Gamitin po ang --help na opsiyon';
$string['cliyesnoprompt'] = 'iteklado ang y (ibig sabihin ay yes/oo) o n (ibig sabihin ay no/hindi)';
-$string['environmentrequireinstall'] = 'ay kinakailangang maluklok/mabuhay';
+$string['environmentrequireinstall'] = 'ay dapat ma-install at ma-enable';
$string['environmentrequireversion'] = 'ang bersiyon {$a->needed} ay kinakailangan at ang pinatatakbo mo ay {$a->current}';
$string['cannotcreatelangdir'] = 'Hindi makalikha ng lang bgsk.';
$string['cannotcreatetempdir'] = 'Hindi makalikha ng temp bgsk.';
-$string['cannotdownloadcomponents'] = 'Hindi mailusong ang mga piyesa';
-$string['cannotdownloadzipfile'] = 'Hindi mailusong ang sakong ZIP.';
-$string['cannotfindcomponent'] = 'Hindi makita ang piyesa.';
-$string['cannotsavemd5file'] = 'Hindi maisilid ang sakong md5.';
-$string['cannotsavezipfile'] = 'Hindi maisilid ang sakong ZIP.';
-$string['cannotunzipfile'] = 'Hindi mai-unzip ang sako.';
-$string['componentisuptodate'] = 'Bago ang piyesa.';
-$string['downloadedfilecheckfailed'] = 'Bigo ang pagsusuri sa inilusong na sako.';
-$string['invalidmd5'] = 'Ditanggap na md5';
-$string['missingrequiredfield'] = 'May ilang nawawalang pitak na kailangan';
+$string['cannotdownloadcomponents'] = 'Hindi mai-download ang mga sangkap';
+$string['cannotdownloadzipfile'] = 'Hindi mai-download ang ZIP file.';
+$string['cannotfindcomponent'] = 'Hindi makita ang component.';
+$string['cannotsavemd5file'] = 'Hindi mai-save ang file na md5.';
+$string['cannotsavezipfile'] = 'Hindi mai-save ang file na ZIP.';
+$string['cannotunzipfile'] = 'Hindi mai-unzip ang file.';
+$string['componentisuptodate'] = 'Up-to-date ang component.';
+$string['downloadedfilecheckfailed'] = 'Bigo ang pagsusuri sa idinownload na file.';
+$string['invalidmd5'] = 'Mali ang check variable - paki-ulit';
+$string['missingrequiredfield'] = 'May ilang nawawalang field na kailangan';
$string['wrongdestpath'] = 'Mali ang patutunguhang landas';
$string['wrongsourcebase'] = 'Mali ang URL base ng source.';
-$string['wrongzipfilename'] = 'Mali ang ngalan ng sako na ZIP';
+$string['wrongzipfilename'] = 'Mali ang pangalan ng ZIP file';
defined('MOODLE_INTERNAL') || die();
-$string['admindirname'] = 'Pang-Admin na Bugsok';
+$string['admindirname'] = 'Pang-Admin na direktoryo';
$string['availablelangs'] = 'Magagamit na mga pakete ng wika';
$string['chooselanguagehead'] = 'Pumilì ng wika';
$string['chooselanguagesub'] = 'Pumili po ng wika para sa pagluluklok LAMANG. Sa mga susunod na iskrin ay makakapili ka ng wika para sa site o tagagamit.';
-$string['dataroot'] = 'Bugsok ng Datos';
-$string['dbprefix'] = 'Unlapi ng mga teybol';
-$string['dirroot'] = 'Bugsok ng Moodle';
+$string['dataroot'] = 'Direktoryo ng Datos';
+$string['dbprefix'] = 'Unlapi ng mga table';
+$string['dirroot'] = 'Direktoryo ng Moodle';
$string['environmenthead'] = 'Sinusuri ang kapaligiran mo...';
$string['installation'] = 'Pagluklok';
$string['langdownloaderror'] = 'Ikinalulungkot namin na ang wikang "{$a}" ay hindi nailuklok. Ang kabuuan ng pagluluklok ay itutuloy sa Ingles.';
$string['memorylimithelp'] = '<p>Ang memory limit ng PHP para sa server mo ay kasalukuyang nakatakda sa {$a}.</p>
-<p>Maaaring magdulot ito ng mga problemang pangmemorya sa Moodle sa mga susunod na panahon, lalo na
- kung marami kang binuhay na modyul at/o marami kang tagagamit.</p>
+<p>Maaaring magdulot ito ng mga problemang pangmemorya sa Moodle sa mga susunod na panahon, lalo na kung marami kang naka-enable na modyul at/o marami kang tagagamit.</p>
-<p>Iminumungkahi namin na isaayos mo ang PHP na may mas mataas na limit kung maaari, tulad ng 40M.
- May iba\'t-ibang paraan na magagawa kayo upang ito ay maiisakatuparan:</p>
+<p>Iminumungkahi namin na isaayos mo ang PHP na may mas mataas na limit kung maaari, tulad ng 40M. May iba\'t-ibang paraan na magagawa ka upang ito ay maisakatuparan:</p>
<ol>
-<li>Kunga maaari mong gawin, muling ikompayl ang PHP na may <i>--enable-memory-limit</i>.
+<li>Kung maaari mong gawin, muling ikompayl ang PHP na may <i>--enable-memory-limit</i>.
Pahihintulutan nito ang Moodle na itakda ang memory limit sa sarili nito.</li>
-<li>Kung mapapasok mo ang iyong sakong php.ini, mababago mo ang <b>memory_limit</b>
- na kaayusan doon at gawin itong mga 40M. Kung wala kang karapatang pasukin ito
+<li>Kung mapapasok mo ang iyong php.ini file, mababago mo ang <b>memory_limit</b>
+ na setting doon at gawin itong mga 40M. Kung wala kang karapatang pasukin ito
baka puwede mong hilingin sa administrador na gawin ito para sa iyo.</li>
-<li>Sa ilang PHP serve maaari kang lumikha ng isang sakong .htaccess sa bugsok ng Moodle
+<li>Sa ilang PHP server maaari kang lumikha ng isang file na .htaccess sa direktoryo ng Moodle
na naglalaman ng linyang ito:
- <p><blockquote>php_value memory_limit 40M</blockquote></p>
+ <blockquote><div>php_value memory_limit 40M</div></blockquote>
<p>Subali\'t sa ilang server ay pipigilin nito ang paggana ng <b>lahat</b> ng pahinang PHP
- (makakakita ka ng mga error kapag tumingin ka sa mga pahina) kaya\'t kakailanganin mong tanggalin ang sakong .htaccess.</p></li>
+ (makakakita ka ng mga error kapag tumingin ka sa mga pahina) kaya\'t kakailanganin mong tanggalin ang .htaccess file.</p></li>
</ol>';
$string['phpversion'] = 'Bersiyon ng PHP';
$string['phpversionhelp'] = '<p>Kinakailangan ng Moodle ang isang bersiyon ng PHP na kahit man lamang 4.3.0. o 5.1.0 (ang 5.0.x ay maraming problema)</p>
$string['welcomep30'] = 'Ang lathala ng <strong>{$a->installername}</strong> na ito ay naglalaman ng mga aplikasyon na lilikha ng kapaligiran na tatakbuhan ng <strong>Moodle</strong>, ito ay ang mga sumusunod:';
$string['welcomep40'] = 'Nilalaman din ng paketeng ito ang <strong>Moodle {$a->moodlerelease} ({$a->moodleversion})</strong>.';
$string['welcomep50'] = 'Ang paggamit ng lahat ng aplikasyon sa paketeng ito ay alinsunod sa kani-kaniyang lisensiya. Ang kumpletong pakete na <strong>{$a->installername}</strong> ay <a href="http://www.opensource.org/docs/definition_plain.html">open source</a> at ipinamamahagi alinsunod sa lisensiyang <a href="http://www.gnu.org/copyleft/gpl.html">GPL</a>';
-$string['welcomep60'] = 'Dadalhin kayo ng mga sumusunod na pahina sa mga madaling hakbang upang maisaayos at mapatakbo ang <strong>Moodle</strong> sa kompyuter ninyo. Kung gusto ninyo ay panatilihin ang umiiral o kaya ay baguhin ito ayon sa inyong pangangailangan.';
+$string['welcomep60'] = 'Dadalhin ka ng mga sumusunod na pahina sa mga madaling sundang hakbang upang maisaayos at mapatakbo ang <strong>Moodle</strong> sa kompyuter mo. Maaari mong tanggapin ang default o kaya ay baguhin ito ayon sa inyong pangangailangan.';
$string['welcomep70'] = 'Iklik ang "Susunod" na buton sa ibaba upang maituloy ang pasasaayos ng <strong>Moodle</strong>.';
$string['wwwroot'] = 'Web address';
$string['order4'] = 'Fourth';
$string['outgoingmailconfig'] = 'Outgoing mail configuration';
$string['overridetossl'] = 'HTTPS for logins has now been deprecated. This instance is now forced to SSL. To remedy this warning change your wwwroot in config.php to https://';
+$string['pageinfodebugsummary'] = 'This page is: {$a}';
$string['passwordchangelogout'] = 'Log out after password change';
$string['passwordchangelogout_desc'] = 'If enabled, when a password is changed, all browser sessions are terminated, apart from the one in which the new password is specified. (This setting does not affect password changes via bulk user upload.)';
$string['passwordchangetokendeletion'] = 'Remove web service access tokens after password change';
$string['prefshow'] = 'Show/hide toggles';
$string['previewrows'] = 'Preview rows';
$string['privacy:metadata:categorieshistory'] = 'A record of previous versions of grade categories';
+$string['privacy:metadata:grade_import_newitem'] = 'Temporary table for storing new grade_item names from grade import';
+$string['privacy:metadata:grade_import_newitem:importcode'] = 'A unique batch code for identifying one batch of imports';
+$string['privacy:metadata:grade_import_newitem:importer'] = 'User importing the data';
+$string['privacy:metadata:grade_import_newitem:itemname'] = 'New grade item name';
+$string['privacy:metadata:grade_import_values'] = 'Temporary table for importing grades';
+$string['privacy:metadata:grade_import_values:feedback'] = 'Grade feedback';
+$string['privacy:metadata:grade_import_values:finalgrade'] = 'Raw grade value';
+$string['privacy:metadata:grade_import_values:importcode'] = 'A unique batch code for identifying one batch of imports';
+$string['privacy:metadata:grade_import_values:importer'] = 'User importing the data';
+$string['privacy:metadata:grade_import_values:importonlyfeedback'] = 'Flag if only feedback was imported';
+$string['privacy:metadata:grade_import_values:userid'] = 'User whose grade was imported';
$string['privacy:metadata:grades'] = 'A record of grades';
$string['privacy:metadata:grades:aggregationstatus'] = 'The aggregation status';
$string['privacy:metadata:grades:aggregationweight'] = 'The weight in aggregation';
$string['privacy:metadata:outcomes:timemodified'] = 'Time at which the record was modified';
$string['privacy:metadata:outcomes:usermodified'] = 'The user who last modified the record';
$string['privacy:metadata:outcomeshistory'] = 'A record of previous versions of outcomes';
+$string['privacy:metadata:scale'] = 'A record of scales';
+$string['privacy:metadata:scale:timemodified'] = 'Time at which the record was last modified';
+$string['privacy:metadata:scale:userid'] = 'The user who last modified the record';
+$string['privacy:metadata:scalehistory'] = 'A record of previous versions of scales';
$string['privacy:path:relatedtome'] = 'Related to me';
$string['privacy:request:historyactiondelete'] = 'Delete';
$string['privacy:request:historyactioninsert'] = 'Insert';
$string['privacy:metadata:events_queue:stackdump'] = 'Any stacktrace associated with this event.';
$string['privacy:metadata:events_queue:timecreated'] = 'The time that this event was created.';
$string['privacy:metadata:events_queue:userid'] = 'The userid associated with this event.';
+$string['privacy:metadata:log'] = 'A collection of past events';
+$string['privacy:metadata:log:action'] = 'A description of the action';
+$string['privacy:metadata:log:cmid'] = 'cmid';
+$string['privacy:metadata:log:course'] = 'course';
+$string['privacy:metadata:log:info'] = 'Additional information';
+$string['privacy:metadata:log:ip'] = 'The IP address used at the time of the event';
+$string['privacy:metadata:log:module'] = 'module';
+$string['privacy:metadata:log:time'] = 'The date at wich the action took place';
+$string['privacy:metadata:log:url'] = 'The URL related to the event';
+$string['privacy:metadata:log:userid'] = 'The ID of the user who performed the action';
$string['privacy:metadata:task_adhoc'] = 'The status of adhoc tasks.';
$string['privacy:metadata:task_adhoc:component'] = 'The component owning the task.';
$string['privacy:metadata:task_adhoc:nextruntime'] = 'The earliest time to run this task.';
$string['privacy:metadata:name'] = 'Name of the preference.';
$string['privacy:metadata:instance'] = 'Identifier for the portfolio.';
$string['privacy:metadata:instancesummary'] = 'This stores portfolio both instances and preferences for the portfolios user is using.';
+$string['privacy:metadata:portfolio_log'] = 'Log of portfolio transfers (used to later check for duplicates)';
+$string['privacy:metadata:portfolio_log:caller_class'] = 'Name of the class used to create the transfer';
+$string['privacy:metadata:portfolio_log:caller_component'] = 'Component name responsible for exporting';
+$string['privacy:metadata:portfolio_log:time'] = 'Time of transfer (in the case of a queued transfer this is the time the actual transfer ran, not when the user started)';
+$string['privacy:metadata:portfolio_log:userid'] = 'User who exported content';
+$string['privacy:metadata:portfolio_tempdata'] = 'Stores temporary data for portfolio exports, cleaned by cron after one day';
+$string['privacy:metadata:portfolio_tempdata:data'] = 'Export data';
+$string['privacy:metadata:portfolio_tempdata:expirytime'] = 'Time this record will expire';
+$string['privacy:metadata:portfolio_tempdata:instance'] = 'Portfolio plugin instance being used';
+$string['privacy:metadata:portfolio_tempdata:userid'] = 'User performing export';
$string['privacy:metadata:value'] = 'Value for the preference';
$string['privacy:metadata:userid'] = 'The user Identifier.';
$string['privacy:path'] = 'Portfolio instances';
$string['privacy:metadata:mnethostid'] = 'An identifier for the mnet host if used.';
$string['privacy:metadata:model'] = 'The device name, occam or iPhone etc..';
$string['privacy:metadata:msn'] = 'The MSN identifier of the user.';
+$string['privacy:metadata:my_pages'] = 'User pages - dashboard and profile. This table does not contain personal data and only used to link dashboard blocks to users';
+$string['privacy:metadata:my_pages:name'] = 'Page name';
+$string['privacy:metadata:my_pages:private'] = 'Whether or not the page is private (dashboard) or public (profile)';
+$string['privacy:metadata:my_pages:userid'] = 'The user who owns this page or 0 for system defaults';
$string['privacy:metadata:password'] = 'The password for this user to log into the system.';
$string['privacy:metadata:passwordresettablesummary'] = 'A table tracking password reset confirmation tokens';
$string['privacy:metadata:passwordtablesummary'] = 'A rotating log of hashes of previously used passwords for the user.';